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.
- checksums.yaml +7 -0
- data/LICENSE.txt +13 -0
- data/README.md +61 -0
- data/lib/launchdarkly-server-sdk.rb +1 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +658 -0
- data/lib/ldclient-rb/context.rb +565 -0
- data/lib/ldclient-rb/evaluation_detail.rb +387 -0
- data/lib/ldclient-rb/events.rb +642 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/flags_state.rb +88 -0
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +166 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +109 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +129 -0
- data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
- data/lib/ldclient-rb/impl/evaluator.rb +539 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +86 -0
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +131 -0
- data/lib/ldclient-rb/impl/event_sender.rb +100 -0
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +170 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +300 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +229 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +306 -0
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +254 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +72 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +46 -0
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +141 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/impl/util.rb +95 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations/consul.rb +45 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +92 -0
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +98 -0
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +663 -0
- data/lib/ldclient-rb/integrations/test_data.rb +213 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +246 -0
- data/lib/ldclient-rb/integrations.rb +6 -0
- data/lib/ldclient-rb/interfaces.rb +974 -0
- data/lib/ldclient-rb/ldclient.rb +822 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +102 -0
- data/lib/ldclient-rb/reference.rb +295 -0
- data/lib/ldclient-rb/requestor.rb +102 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +196 -0
- data/lib/ldclient-rb/util.rb +132 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/lib/ldclient-rb.rb +27 -0
- metadata +400 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# Simple implementation of a thread-safe memoized value whose generator function will never be
|
4
|
+
# run more than once, and whose value can be overridden by explicit assignment.
|
5
|
+
# Note that we no longer use this class and it will be removed in a future version.
|
6
|
+
# @private
|
7
|
+
class MemoizedValue
|
8
|
+
def initialize(&generator)
|
9
|
+
@generator = generator
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@inited = false
|
12
|
+
@value = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def get
|
16
|
+
@mutex.synchronize do
|
17
|
+
unless @inited
|
18
|
+
@value = @generator.call
|
19
|
+
@inited = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@value
|
23
|
+
end
|
24
|
+
|
25
|
+
def set(value)
|
26
|
+
@mutex.synchronize do
|
27
|
+
@value = value
|
28
|
+
@inited = true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'ldclient-rb/impl/migrations/migrator'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
#
|
5
|
+
# Namespace for feature-flag based technology migration support.
|
6
|
+
#
|
7
|
+
module Migrations
|
8
|
+
# Symbol representing the old origin, or the old technology source you are migrating away from.
|
9
|
+
ORIGIN_OLD = :old
|
10
|
+
# Symbol representing the new origin, or the new technology source you are migrating towards.
|
11
|
+
ORIGIN_NEW = :new
|
12
|
+
|
13
|
+
# Symbol defining a read-related operation
|
14
|
+
OP_READ = :read
|
15
|
+
# Symbol defining a write-related operation
|
16
|
+
OP_WRITE = :write
|
17
|
+
|
18
|
+
STAGE_OFF = :off
|
19
|
+
STAGE_DUALWRITE = :dualwrite
|
20
|
+
STAGE_SHADOW = :shadow
|
21
|
+
STAGE_LIVE = :live
|
22
|
+
STAGE_RAMPDOWN = :rampdown
|
23
|
+
STAGE_COMPLETE = :complete
|
24
|
+
|
25
|
+
VALID_OPERATIONS = [
|
26
|
+
OP_READ,
|
27
|
+
OP_WRITE,
|
28
|
+
]
|
29
|
+
|
30
|
+
VALID_ORIGINS = [
|
31
|
+
ORIGIN_OLD,
|
32
|
+
ORIGIN_NEW,
|
33
|
+
]
|
34
|
+
|
35
|
+
VALID_STAGES = [
|
36
|
+
STAGE_OFF,
|
37
|
+
STAGE_DUALWRITE,
|
38
|
+
STAGE_SHADOW,
|
39
|
+
STAGE_LIVE,
|
40
|
+
STAGE_RAMPDOWN,
|
41
|
+
STAGE_COMPLETE,
|
42
|
+
]
|
43
|
+
|
44
|
+
#
|
45
|
+
# The OperationResult wraps the {LaunchDarkly::Result} class to tie an operation origin to a result.
|
46
|
+
#
|
47
|
+
class OperationResult
|
48
|
+
extend Forwardable
|
49
|
+
def_delegators :@result, :value, :error, :exception, :success?
|
50
|
+
|
51
|
+
#
|
52
|
+
# @param origin [Symbol]
|
53
|
+
# @param result [LaunchDarkly::Result]
|
54
|
+
#
|
55
|
+
def initialize(origin, result)
|
56
|
+
@origin = origin
|
57
|
+
@result = result
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# @return [Symbol] The origin this result is associated with.
|
62
|
+
#
|
63
|
+
attr_reader :origin
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# A write result contains the operation results against both the authoritative and non-authoritative origins.
|
68
|
+
#
|
69
|
+
# Authoritative writes are always executed first. In the event of a failure, the non-authoritative write will not
|
70
|
+
# be executed, resulting in a nil value in the final WriteResult.
|
71
|
+
#
|
72
|
+
class WriteResult
|
73
|
+
#
|
74
|
+
# @param authoritative [OperationResult]
|
75
|
+
# @param nonauthoritative [OperationResult, nil]
|
76
|
+
#
|
77
|
+
def initialize(authoritative, nonauthoritative = nil)
|
78
|
+
@authoritative = authoritative
|
79
|
+
@nonauthoritative = nonauthoritative
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Returns the operation result for the authoritative origin.
|
84
|
+
#
|
85
|
+
# @return [OperationResult]
|
86
|
+
#
|
87
|
+
attr_reader :authoritative
|
88
|
+
|
89
|
+
#
|
90
|
+
# Returns the operation result for the non-authoritative origin.
|
91
|
+
#
|
92
|
+
# This result might be nil as the non-authoritative write does not execute in every stage, and will not execute
|
93
|
+
# if the authoritative write failed.
|
94
|
+
#
|
95
|
+
# @return [OperationResult, nil]
|
96
|
+
#
|
97
|
+
attr_reader :nonauthoritative
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
#
|
102
|
+
# The migration builder is used to configure and construct an instance of a
|
103
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator}. This migrator can be used to perform LaunchDarkly assisted
|
104
|
+
# technology migrations through the use of migration-based feature flags.
|
105
|
+
#
|
106
|
+
class MigratorBuilder
|
107
|
+
EXECUTION_SERIAL = :serial
|
108
|
+
EXECUTION_RANDOM = :random
|
109
|
+
EXECUTION_PARALLEL = :parallel
|
110
|
+
|
111
|
+
VALID_EXECUTION_ORDERS = [EXECUTION_SERIAL, EXECUTION_RANDOM, EXECUTION_PARALLEL]
|
112
|
+
private_constant :VALID_EXECUTION_ORDERS
|
113
|
+
|
114
|
+
#
|
115
|
+
# @param client [LaunchDarkly::LDClient]
|
116
|
+
#
|
117
|
+
def initialize(client)
|
118
|
+
@client = client
|
119
|
+
|
120
|
+
# Default settings as required by the spec
|
121
|
+
@read_execution_order = EXECUTION_PARALLEL
|
122
|
+
@measure_latency = true
|
123
|
+
@measure_errors = true
|
124
|
+
|
125
|
+
@read_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
|
126
|
+
@write_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# The read execution order influences the parallelism and execution order for read operations involving multiple
|
131
|
+
# origins.
|
132
|
+
#
|
133
|
+
# @param order [Symbol]
|
134
|
+
#
|
135
|
+
def read_execution_order(order)
|
136
|
+
return unless VALID_EXECUTION_ORDERS.include? order
|
137
|
+
|
138
|
+
@read_execution_order = order
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Enable or disable latency tracking for migration operations. This latency information can be sent upstream to
|
143
|
+
# LaunchDarkly to enhance migration visibility.
|
144
|
+
#
|
145
|
+
# @param enabled [Boolean]
|
146
|
+
#
|
147
|
+
def track_latency(enabled)
|
148
|
+
@measure_latency = !!enabled
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Enable or disable error tracking for migration operations. This error information can be sent upstream to
|
153
|
+
# LaunchDarkly to enhance migration visibility.
|
154
|
+
#
|
155
|
+
# @param enabled [Boolean]
|
156
|
+
#
|
157
|
+
def track_errors(enabled)
|
158
|
+
@measure_errors = !!enabled
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Read can be used to configure the migration-read behavior of the resulting
|
163
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
|
164
|
+
#
|
165
|
+
# Users are required to provide two different read methods -- one to read from the old migration origin, and one
|
166
|
+
# to read from the new origin. Additionally, customers can opt-in to consistency tracking by providing a
|
167
|
+
# comparison function.
|
168
|
+
#
|
169
|
+
# Depending on the migration stage, one or both of these read methods may be called.
|
170
|
+
#
|
171
|
+
# The read methods should accept a single nullable parameter. This parameter is a payload passed through the
|
172
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator#read} method. This method should return a {LaunchDarkly::Result}
|
173
|
+
# instance.
|
174
|
+
#
|
175
|
+
# The consistency method should accept 2 parameters of any type. These parameters are the results of executing the
|
176
|
+
# read operation against the old and new origins. If both operations were successful, the consistency method will
|
177
|
+
# be invoked. This method should return true if the two parameters are equal, or false otherwise.
|
178
|
+
#
|
179
|
+
# @param old_read [#call]
|
180
|
+
# @param new_read [#call]
|
181
|
+
# @param comparison [#call, nil]
|
182
|
+
#
|
183
|
+
def read(old_read, new_read, comparison = nil)
|
184
|
+
return unless old_read.respond_to?(:call) && old_read.arity == 1
|
185
|
+
return unless new_read.respond_to?(:call) && new_read.arity == 1
|
186
|
+
return unless comparison.nil? || (comparison.respond_to?(:call) && comparison.arity == 2)
|
187
|
+
|
188
|
+
@read_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_read, new_read, comparison)
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Write can be used to configure the migration-write behavior of the resulting
|
193
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
|
194
|
+
#
|
195
|
+
# Users are required to provide two different write methods -- one to write to the old migration origin, and one
|
196
|
+
# to write to the new origin.
|
197
|
+
#
|
198
|
+
# Depending on the migration stage, one or both of these write methods may be called.
|
199
|
+
#
|
200
|
+
# The write methods should accept a single nullable parameter. This parameter is a payload passed through the
|
201
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator#write} method. This method should return a {LaunchDarkly::Result}
|
202
|
+
# instance.
|
203
|
+
#
|
204
|
+
# @param old_write [#call]
|
205
|
+
# @param new_write [#call]
|
206
|
+
#
|
207
|
+
def write(old_write, new_write)
|
208
|
+
return unless old_write.respond_to?(:call) && old_write.arity == 1
|
209
|
+
return unless new_write.respond_to?(:call) && new_write.arity == 1
|
210
|
+
|
211
|
+
@write_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_write, new_write, nil)
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Build constructs a {LaunchDarkly::Interfaces::Migrations::Migrator} instance to support migration-based reads
|
216
|
+
# and writes. A string describing any failure conditions will be returned if the build fails.
|
217
|
+
#
|
218
|
+
# @return [LaunchDarkly::Interfaces::Migrations::Migrator, string]
|
219
|
+
#
|
220
|
+
def build
|
221
|
+
return "client not provided" if @client.nil?
|
222
|
+
return "read configuration not provided" if @read_config.nil?
|
223
|
+
return "write configuration not provided" if @write_config.nil?
|
224
|
+
|
225
|
+
LaunchDarkly::Impl::Migrations::Migrator.new(@client, @read_execution_order, @read_config, @write_config, @measure_latency, @measure_errors)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
require "concurrent/atomics"
|
3
|
+
require "concurrent/executors"
|
4
|
+
require "thread"
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
# Simple wrapper for a FixedThreadPool that rejects new jobs if all the threads are busy, rather
|
8
|
+
# than blocking. Also provides a way to wait for all jobs to finish without shutting down.
|
9
|
+
# @private
|
10
|
+
class NonBlockingThreadPool
|
11
|
+
def initialize(capacity)
|
12
|
+
@capacity = capacity
|
13
|
+
@pool = Concurrent::FixedThreadPool.new(capacity)
|
14
|
+
@semaphore = Concurrent::Semaphore.new(capacity)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Attempts to submit a job, but only if a worker is available. Unlike the regular post method,
|
18
|
+
# this returns a value: true if the job was submitted, false if all workers are busy.
|
19
|
+
def post
|
20
|
+
unless @semaphore.try_acquire(1)
|
21
|
+
return
|
22
|
+
end
|
23
|
+
@pool.post do
|
24
|
+
begin
|
25
|
+
yield
|
26
|
+
ensure
|
27
|
+
@semaphore.release(1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Waits until no jobs are executing, without shutting down the pool.
|
33
|
+
def wait_all
|
34
|
+
@semaphore.acquire(@capacity)
|
35
|
+
@semaphore.release(@capacity)
|
36
|
+
end
|
37
|
+
|
38
|
+
def shutdown
|
39
|
+
@pool.shutdown
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_termination
|
43
|
+
@pool.wait_for_termination
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "ldclient-rb/impl/repeating_task"
|
2
|
+
|
3
|
+
require "concurrent/atomics"
|
4
|
+
require "json"
|
5
|
+
require "thread"
|
6
|
+
|
7
|
+
module LaunchDarkly
|
8
|
+
# @private
|
9
|
+
class PollingProcessor
|
10
|
+
def initialize(config, requestor)
|
11
|
+
@config = config
|
12
|
+
@requestor = requestor
|
13
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
14
|
+
@started = Concurrent::AtomicBoolean.new(false)
|
15
|
+
@ready = Concurrent::Event.new
|
16
|
+
@task = Impl::RepeatingTask.new(@config.poll_interval, 0, -> { self.poll }, @config.logger)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialized?
|
20
|
+
@initialized.value
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
return @ready unless @started.make_true
|
25
|
+
@config.logger.info { "[LDClient] Initializing polling connection" }
|
26
|
+
@task.start
|
27
|
+
@ready
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
stop_with_error_info
|
32
|
+
end
|
33
|
+
|
34
|
+
def poll
|
35
|
+
begin
|
36
|
+
all_data = @requestor.request_all_data
|
37
|
+
if all_data
|
38
|
+
update_sink_or_data_store.init(all_data)
|
39
|
+
if @initialized.make_true
|
40
|
+
@config.logger.info { "[LDClient] Polling connection initialized" }
|
41
|
+
@ready.set
|
42
|
+
end
|
43
|
+
end
|
44
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
45
|
+
rescue JSON::ParserError => e
|
46
|
+
@config.logger.error { "[LDClient] JSON parsing failed for polling response." }
|
47
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
48
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
49
|
+
0,
|
50
|
+
e.to_s,
|
51
|
+
Time.now
|
52
|
+
)
|
53
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
|
54
|
+
rescue UnexpectedResponseError => e
|
55
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
56
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, e.status, nil, Time.now)
|
57
|
+
message = Util.http_error_message(e.status, "polling request", "will retry")
|
58
|
+
@config.logger.error { "[LDClient] #{message}" }
|
59
|
+
|
60
|
+
if Util.http_error_recoverable?(e.status)
|
61
|
+
@config.data_source_update_sink&.update_status(
|
62
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
63
|
+
error_info
|
64
|
+
)
|
65
|
+
else
|
66
|
+
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
67
|
+
stop_with_error_info error_info
|
68
|
+
end
|
69
|
+
rescue StandardError => e
|
70
|
+
Util.log_exception(@config.logger, "Exception while polling", e)
|
71
|
+
@config.data_source_update_sink&.update_status(
|
72
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
73
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, e.to_s, Time.now)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# The original implementation of this class relied on the feature store
|
80
|
+
# directly, which we are trying to move away from. Customers who might have
|
81
|
+
# instantiated this directly for some reason wouldn't know they have to set
|
82
|
+
# the config's sink manually, so we have to fall back to the store if the
|
83
|
+
# sink isn't present.
|
84
|
+
#
|
85
|
+
# The next major release should be able to simplify this structure and
|
86
|
+
# remove the need for fall back to the data store because the update sink
|
87
|
+
# should always be present.
|
88
|
+
#
|
89
|
+
private def update_sink_or_data_store
|
90
|
+
@config.data_source_update_sink || @config.feature_store
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
|
95
|
+
#
|
96
|
+
private def stop_with_error_info(error_info = nil)
|
97
|
+
@task.stop
|
98
|
+
@config.logger.info { "[LDClient] Polling connection stopped" }
|
99
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
#
|
3
|
+
# Reference is an attribute name or path expression identifying a value
|
4
|
+
# within a Context.
|
5
|
+
#
|
6
|
+
# This type is mainly intended to be used internally by LaunchDarkly SDK and
|
7
|
+
# service code, where efficiency is a major concern so it's desirable to do
|
8
|
+
# any parsing or preprocessing just once. Applications are unlikely to need
|
9
|
+
# to use the Reference type directly.
|
10
|
+
#
|
11
|
+
# It can be used to retrieve a value with LDContext.get_value_for_reference()
|
12
|
+
# or to identify an attribute or nested value that should be considered
|
13
|
+
# private.
|
14
|
+
#
|
15
|
+
# Parsing and validation are done at the time that the Reference is
|
16
|
+
# constructed. If a Reference instance was created from an invalid string, it
|
17
|
+
# is considered invalid and its {Reference#error} attribute will return a
|
18
|
+
# non-nil error.
|
19
|
+
#
|
20
|
+
# ## Syntax
|
21
|
+
#
|
22
|
+
# The string representation of an attribute reference in LaunchDarkly JSON
|
23
|
+
# data uses the following syntax:
|
24
|
+
#
|
25
|
+
# If the first character is not a slash, the string is interpreted literally
|
26
|
+
# as an attribute name. An attribute name can contain any characters, but
|
27
|
+
# must not be empty.
|
28
|
+
#
|
29
|
+
# If the first character is a slash, the string is interpreted as a
|
30
|
+
# slash-delimited path where the first path component is an attribute name,
|
31
|
+
# and each subsequent path component is the name of a property in a JSON
|
32
|
+
# object. Any instances of the characters "/" or "~" in a path component are
|
33
|
+
# escaped as "~1" or "~0" respectively. This syntax deliberately resembles
|
34
|
+
# JSON Pointer, but no JSON Pointer behaviors other than those mentioned here
|
35
|
+
# are supported.
|
36
|
+
#
|
37
|
+
# ## Examples
|
38
|
+
#
|
39
|
+
# Suppose there is a context whose JSON implementation looks like this:
|
40
|
+
#
|
41
|
+
# {
|
42
|
+
# "kind": "user",
|
43
|
+
# "key": "value1",
|
44
|
+
# "address": {
|
45
|
+
# "street": {
|
46
|
+
# "line1": "value2",
|
47
|
+
# "line2": "value3"
|
48
|
+
# },
|
49
|
+
# "city": "value4"
|
50
|
+
# },
|
51
|
+
# "good/bad": "value5"
|
52
|
+
# }
|
53
|
+
#
|
54
|
+
# The attribute references "key" and "/key" would both point to "value1".
|
55
|
+
#
|
56
|
+
# The attribute reference "/address/street/line1" would point to "value2".
|
57
|
+
#
|
58
|
+
# The attribute references "good/bad" and "/good~1bad" would both point to
|
59
|
+
# "value5".
|
60
|
+
#
|
61
|
+
class Reference
|
62
|
+
ERR_EMPTY = 'empty reference'
|
63
|
+
private_constant :ERR_EMPTY
|
64
|
+
|
65
|
+
ERR_INVALID_ESCAPE_SEQUENCE = 'invalid escape sequence'
|
66
|
+
private_constant :ERR_INVALID_ESCAPE_SEQUENCE
|
67
|
+
|
68
|
+
ERR_DOUBLE_TRAILING_SLASH = 'double or trailing slash'
|
69
|
+
private_constant :ERR_DOUBLE_TRAILING_SLASH
|
70
|
+
|
71
|
+
#
|
72
|
+
# Returns nil for a valid Reference, or a non-nil error value for an
|
73
|
+
# invalid Reference.
|
74
|
+
#
|
75
|
+
# A Reference is invalid if the input string is empty, or starts with a
|
76
|
+
# slash but is not a valid slash-delimited path, or starts with a slash and
|
77
|
+
# contains an invalid escape sequence.
|
78
|
+
#
|
79
|
+
# Otherwise, the Reference is valid, but that does not guarantee that such
|
80
|
+
# an attribute exists in any given Context. For instance,
|
81
|
+
# Reference.create("name") is a valid Reference, but a specific Context
|
82
|
+
# might or might not have a name.
|
83
|
+
#
|
84
|
+
# See comments on the Reference type for more details of the attribute
|
85
|
+
# reference syntax.
|
86
|
+
#
|
87
|
+
# @return [String, nil]
|
88
|
+
#
|
89
|
+
attr_reader :error
|
90
|
+
|
91
|
+
#
|
92
|
+
# Returns the attribute reference as a string, in the same format provided
|
93
|
+
# to {#create}.
|
94
|
+
#
|
95
|
+
# If the Reference was created with {#create}, this value is identical to
|
96
|
+
# the original string. If it was created with {#create_literal}, the value
|
97
|
+
# may be different due to unescaping (for instance, an attribute whose name
|
98
|
+
# is "/a" would be represented as "~1a").
|
99
|
+
#
|
100
|
+
# @return [String, nil]
|
101
|
+
#
|
102
|
+
attr_reader :raw_path
|
103
|
+
|
104
|
+
def initialize(raw_path, components = [], error = nil)
|
105
|
+
@raw_path = raw_path
|
106
|
+
# @type [Array<Symbol>]
|
107
|
+
@components = components
|
108
|
+
@error = error
|
109
|
+
end
|
110
|
+
private_class_method :new
|
111
|
+
|
112
|
+
protected attr_reader :components
|
113
|
+
|
114
|
+
#
|
115
|
+
# Creates a Reference from a string. For the supported syntax and examples,
|
116
|
+
# see comments on the Reference type.
|
117
|
+
#
|
118
|
+
# This constructor always returns a Reference that preserves the original
|
119
|
+
# string, even if validation fails, so that accessing {#raw_path} (or
|
120
|
+
# serializing the Reference to JSON) will produce the original string. If
|
121
|
+
# validation fails, {#error} will return a non-nil error and any SDK method
|
122
|
+
# that takes this Reference as a parameter will consider it invalid.
|
123
|
+
#
|
124
|
+
# @param value [String, Symbol]
|
125
|
+
# @return [Reference]
|
126
|
+
#
|
127
|
+
def self.create(value)
|
128
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
129
|
+
return new(value, [], ERR_EMPTY)
|
130
|
+
end
|
131
|
+
|
132
|
+
value = value.to_s if value.is_a?(Symbol)
|
133
|
+
|
134
|
+
return new(value, [], ERR_EMPTY) if value.empty? || value == "/"
|
135
|
+
|
136
|
+
unless value.start_with? "/"
|
137
|
+
return new(value, [value.to_sym])
|
138
|
+
end
|
139
|
+
|
140
|
+
if value.end_with? "/"
|
141
|
+
return new(value, [], ERR_DOUBLE_TRAILING_SLASH)
|
142
|
+
end
|
143
|
+
|
144
|
+
components = []
|
145
|
+
value[1..].split("/").each do |component|
|
146
|
+
if component.empty?
|
147
|
+
return new(value, [], ERR_DOUBLE_TRAILING_SLASH)
|
148
|
+
end
|
149
|
+
|
150
|
+
path, error = unescape_path(component)
|
151
|
+
|
152
|
+
if error
|
153
|
+
return new(value, [], error)
|
154
|
+
end
|
155
|
+
|
156
|
+
components << path.to_sym
|
157
|
+
end
|
158
|
+
|
159
|
+
new(value, components)
|
160
|
+
end
|
161
|
+
|
162
|
+
#
|
163
|
+
# create_literal is similar to {#create} except that it always
|
164
|
+
# interprets the string as a literal attribute name, never as a
|
165
|
+
# slash-delimited path expression. There is no escaping or unescaping, even
|
166
|
+
# if the name contains literal '/' or '~' characters. Since an attribute
|
167
|
+
# name can contain any characters, this method always returns a valid
|
168
|
+
# Reference unless the name is empty.
|
169
|
+
#
|
170
|
+
# For example: Reference.create_literal("name") is exactly equivalent to
|
171
|
+
# Reference.create("name"). Reference.create_literal("a/b") is exactly
|
172
|
+
# equivalent to Reference.create("a/b") (since the syntax used by {#create}
|
173
|
+
# treats the whole string as a literal as long as it does not start with a
|
174
|
+
# slash), or to Reference.create("/a~1b").
|
175
|
+
#
|
176
|
+
# @param value [String, Symbol]
|
177
|
+
# @return [Reference]
|
178
|
+
#
|
179
|
+
def self.create_literal(value)
|
180
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
181
|
+
return new(value, [], ERR_EMPTY)
|
182
|
+
end
|
183
|
+
|
184
|
+
value = value.to_s if value.is_a?(Symbol)
|
185
|
+
|
186
|
+
return new(value, [], ERR_EMPTY) if value.empty?
|
187
|
+
return new(value, [value.to_sym]) if value[0] != '/'
|
188
|
+
|
189
|
+
escaped = "/" + value.gsub('~', '~0').gsub('/', '~1')
|
190
|
+
new(escaped, [value.to_sym])
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# Returns the number of path components in the Reference.
|
195
|
+
#
|
196
|
+
# For a simple attribute reference such as "name" with no leading slash,
|
197
|
+
# this returns 1.
|
198
|
+
#
|
199
|
+
# For an attribute reference with a leading slash, it is the number of
|
200
|
+
# slash-delimited path components after the initial slash. For instance,
|
201
|
+
# NewRef("/a/b").Depth() returns 2.
|
202
|
+
#
|
203
|
+
# @return [Integer]
|
204
|
+
#
|
205
|
+
def depth
|
206
|
+
@components.size
|
207
|
+
end
|
208
|
+
|
209
|
+
#
|
210
|
+
# Retrieves a single path component from the attribute reference.
|
211
|
+
#
|
212
|
+
# For a simple attribute reference such as "name" with no leading slash, if
|
213
|
+
# index is zero, {#component} returns the attribute name as a symbol.
|
214
|
+
#
|
215
|
+
# For an attribute reference with a leading slash, if index is non-negative
|
216
|
+
# and less than {#depth}, Component returns the path component as a symbol.
|
217
|
+
#
|
218
|
+
# If index is out of range, it returns nil.
|
219
|
+
#
|
220
|
+
# Reference.create("a").component(0) # returns "a"
|
221
|
+
# Reference.create("/a/b").component(1) # returns "b"
|
222
|
+
#
|
223
|
+
# @param index [Integer]
|
224
|
+
# @return [Symbol, nil]
|
225
|
+
#
|
226
|
+
def component(index)
|
227
|
+
return nil if index < 0 || index >= depth
|
228
|
+
|
229
|
+
@components[index]
|
230
|
+
end
|
231
|
+
|
232
|
+
def ==(other)
|
233
|
+
self.error == other.error && self.components == other.components
|
234
|
+
end
|
235
|
+
alias eql? ==
|
236
|
+
|
237
|
+
def hash
|
238
|
+
([error] + components).hash
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# Convert the Reference to a JSON string.
|
243
|
+
#
|
244
|
+
# @param args [Array]
|
245
|
+
# @return [String]
|
246
|
+
#
|
247
|
+
def to_json(*args)
|
248
|
+
JSON.generate(@raw_path, *args)
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# Performs unescaping of attribute reference path components:
|
253
|
+
#
|
254
|
+
# "~1" becomes "/"
|
255
|
+
# "~0" becomes "~"
|
256
|
+
# "~" followed by any character other than "0" or "1" is invalid
|
257
|
+
#
|
258
|
+
# This method returns an array of two values. The first element of the
|
259
|
+
# array is the path if unescaping was valid; otherwise, it will be nil. The
|
260
|
+
# second value is an error string, or nil if the unescaping was successful.
|
261
|
+
#
|
262
|
+
# @param path [String]
|
263
|
+
# @return [Array([String, nil], [String, nil])] Returns a fixed size array.
|
264
|
+
#
|
265
|
+
private_class_method def self.unescape_path(path)
|
266
|
+
# If there are no tildes then there's definitely nothing to do
|
267
|
+
return path, nil unless path.include? '~'
|
268
|
+
|
269
|
+
out = ""
|
270
|
+
i = 0
|
271
|
+
while i < path.size
|
272
|
+
if path[i] != "~"
|
273
|
+
out << path[i]
|
274
|
+
i += 1
|
275
|
+
next
|
276
|
+
end
|
277
|
+
|
278
|
+
return nil, ERR_INVALID_ESCAPE_SEQUENCE if i + 1 == path.size
|
279
|
+
|
280
|
+
case path[i + 1]
|
281
|
+
when '0'
|
282
|
+
out << "~"
|
283
|
+
when '1'
|
284
|
+
out << '/'
|
285
|
+
else
|
286
|
+
return nil, ERR_INVALID_ESCAPE_SEQUENCE
|
287
|
+
end
|
288
|
+
|
289
|
+
i += 2
|
290
|
+
end
|
291
|
+
|
292
|
+
[out, nil]
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|