launchdarkly-server-sdk 5.8.2 → 6.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 +4 -4
- data/.circleci/config.yml +28 -122
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +7 -3
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +69 -42
- data/README.md +2 -2
- data/azure-pipelines.yml +1 -1
- data/launchdarkly-server-sdk.gemspec +16 -16
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +1 -4
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/ldclient.rb +14 -9
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -15
- data/lib/ldclient-rb/stream.rb +9 -6
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +10 -8
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -11
- metadata +89 -46
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/spec/evaluation_spec.rb +0 -789
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
module Impl
|
4
|
+
module Model
|
5
|
+
# Abstraction of deserializing a feature flag or segment that was read from a data store or
|
6
|
+
# received from LaunchDarkly.
|
7
|
+
def self.deserialize(kind, json)
|
8
|
+
return nil if json.nil?
|
9
|
+
item = JSON.parse(json, symbolize_names: true)
|
10
|
+
postprocess_item_after_deserializing!(kind, item)
|
11
|
+
item
|
12
|
+
end
|
13
|
+
|
14
|
+
# Abstraction of serializing a feature flag or segment that will be written to a data store.
|
15
|
+
# Currently we just call to_json.
|
16
|
+
def self.serialize(kind, item)
|
17
|
+
item.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
# Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
|
21
|
+
def self.make_all_store_data(received_data)
|
22
|
+
flags = received_data[:flags]
|
23
|
+
postprocess_items_after_deserializing!(FEATURES, flags)
|
24
|
+
segments = received_data[:segments]
|
25
|
+
postprocess_items_after_deserializing!(SEGMENTS, segments)
|
26
|
+
{ FEATURES => flags, SEGMENTS => segments }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
|
30
|
+
# or read it from a persistent data store). This allows us to precompute some derived attributes that
|
31
|
+
# will never change during the lifetime of that item.
|
32
|
+
def self.postprocess_item_after_deserializing!(kind, item)
|
33
|
+
return if !item
|
34
|
+
# Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
|
35
|
+
# to the "kind" object or the item class.
|
36
|
+
if kind.eql? FEATURES
|
37
|
+
# For feature flags, we precompute all possible parameterized EvaluationReason instances.
|
38
|
+
prereqs = item[:prerequisites]
|
39
|
+
if !prereqs.nil?
|
40
|
+
prereqs.each do |prereq|
|
41
|
+
prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rules = item[:rules]
|
45
|
+
if !rules.nil?
|
46
|
+
rules.each_index do |i|
|
47
|
+
rule = rules[i]
|
48
|
+
rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.postprocess_items_after_deserializing!(kind, items_map)
|
55
|
+
return items_map if !items_map
|
56
|
+
items_map.each do |key, item|
|
57
|
+
postprocess_item_after_deserializing!(kind, item)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module Impl
|
3
|
+
# A simple thread safe generic unbounded resource pool abstraction
|
4
|
+
class UnboundedPool
|
5
|
+
def initialize(instance_creator, instance_destructor)
|
6
|
+
@pool = Array.new
|
7
|
+
@lock = Mutex.new
|
8
|
+
@instance_creator = instance_creator
|
9
|
+
@instance_destructor = instance_destructor
|
10
|
+
end
|
11
|
+
|
12
|
+
def acquire
|
13
|
+
@lock.synchronize {
|
14
|
+
if @pool.length == 0
|
15
|
+
@instance_creator.call()
|
16
|
+
else
|
17
|
+
@pool.pop()
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def release(instance)
|
23
|
+
@lock.synchronize { @pool.push(instance) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def dispose_all
|
27
|
+
@lock.synchronize {
|
28
|
+
@pool.map { |instance| @instance_destructor.call(instance) } if !@instance_destructor.nil?
|
29
|
+
@pool.clear()
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "ldclient-rb/impl/diagnostic_events"
|
2
|
+
require "ldclient-rb/impl/evaluator"
|
2
3
|
require "ldclient-rb/impl/event_factory"
|
3
4
|
require "ldclient-rb/impl/store_client_wrapper"
|
4
5
|
require "concurrent/atomics"
|
@@ -14,7 +15,6 @@ module LaunchDarkly
|
|
14
15
|
# should create a single client instance for the lifetime of the application.
|
15
16
|
#
|
16
17
|
class LDClient
|
17
|
-
include Evaluation
|
18
18
|
include Impl
|
19
19
|
#
|
20
20
|
# Creates a new client instance that connects to LaunchDarkly. A custom
|
@@ -57,6 +57,10 @@ module LaunchDarkly
|
|
57
57
|
updated_config.instance_variable_set(:@feature_store, @store)
|
58
58
|
@config = updated_config
|
59
59
|
|
60
|
+
get_flag = lambda { |key| @store.get(FEATURES, key) }
|
61
|
+
get_segment = lambda { |key| @store.get(SEGMENTS, key) }
|
62
|
+
@evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, @config.logger)
|
63
|
+
|
60
64
|
if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
|
61
65
|
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
|
62
66
|
else
|
@@ -333,12 +337,13 @@ module LaunchDarkly
|
|
333
337
|
next
|
334
338
|
end
|
335
339
|
begin
|
336
|
-
result = evaluate(f, user, @
|
340
|
+
result = @evaluator.evaluate(f, user, @event_factory_default)
|
337
341
|
state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
|
338
342
|
details_only_if_tracked)
|
339
343
|
rescue => exn
|
340
344
|
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
|
341
|
-
state.add_flag(f, nil, nil, with_reasons ?
|
345
|
+
state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
|
346
|
+
details_only_if_tracked)
|
342
347
|
end
|
343
348
|
end
|
344
349
|
|
@@ -376,7 +381,7 @@ module LaunchDarkly
|
|
376
381
|
# @return [EvaluationDetail]
|
377
382
|
def evaluate_internal(key, user, default, event_factory)
|
378
383
|
if @config.offline?
|
379
|
-
return error_result(
|
384
|
+
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
380
385
|
end
|
381
386
|
|
382
387
|
if !initialized?
|
@@ -384,7 +389,7 @@ module LaunchDarkly
|
|
384
389
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
385
390
|
else
|
386
391
|
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
|
387
|
-
detail = error_result(
|
392
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
388
393
|
@event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
|
389
394
|
return detail
|
390
395
|
end
|
@@ -394,20 +399,20 @@ module LaunchDarkly
|
|
394
399
|
|
395
400
|
if feature.nil?
|
396
401
|
@config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
|
397
|
-
detail = error_result(
|
402
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
|
398
403
|
@event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
|
399
404
|
return detail
|
400
405
|
end
|
401
406
|
|
402
407
|
unless user
|
403
408
|
@config.logger.error { "[LDClient] Must specify user" }
|
404
|
-
detail = error_result(
|
409
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
405
410
|
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
|
406
411
|
return detail
|
407
412
|
end
|
408
413
|
|
409
414
|
begin
|
410
|
-
res = evaluate(feature, user,
|
415
|
+
res = @evaluator.evaluate(feature, user, event_factory)
|
411
416
|
if !res.events.nil?
|
412
417
|
res.events.each do |event|
|
413
418
|
@event_processor.add_event(event)
|
@@ -421,7 +426,7 @@ module LaunchDarkly
|
|
421
426
|
return detail
|
422
427
|
rescue => exn
|
423
428
|
Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
|
424
|
-
detail = error_result(
|
429
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
|
425
430
|
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
|
426
431
|
return detail
|
427
432
|
end
|
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -37,10 +37,7 @@ module LaunchDarkly
|
|
37
37
|
def poll
|
38
38
|
all_data = @requestor.request_all_data
|
39
39
|
if all_data
|
40
|
-
@config.feature_store.init(
|
41
|
-
FEATURES => all_data[:flags],
|
42
|
-
SEGMENTS => all_data[:segments]
|
43
|
-
})
|
40
|
+
@config.feature_store.init(all_data)
|
44
41
|
if @initialized.make_true
|
45
42
|
@config.logger.info { "[LDClient] Polling connection initialized" }
|
46
43
|
@ready.set
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "json"
|
3
5
|
require "uri"
|
6
|
+
require "http"
|
4
7
|
|
5
8
|
module LaunchDarkly
|
6
9
|
# @private
|
@@ -22,37 +25,44 @@ module LaunchDarkly
|
|
22
25
|
def initialize(sdk_key, config)
|
23
26
|
@sdk_key = sdk_key
|
24
27
|
@config = config
|
25
|
-
@
|
28
|
+
@http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
|
26
29
|
@cache = @config.cache_store
|
27
30
|
end
|
28
31
|
|
29
32
|
def request_all_data()
|
30
|
-
make_request("/sdk/latest-all")
|
33
|
+
all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
|
34
|
+
Impl::Model.make_all_store_data(all_data)
|
31
35
|
end
|
32
36
|
|
33
37
|
def stop
|
34
38
|
begin
|
35
|
-
@
|
39
|
+
@http_client.close
|
36
40
|
rescue
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
40
44
|
private
|
41
45
|
|
46
|
+
def request_single_item(kind, path)
|
47
|
+
Impl::Model.deserialize(kind, make_request(path))
|
48
|
+
end
|
49
|
+
|
42
50
|
def make_request(path)
|
43
|
-
@client.start if !@client.started?
|
44
51
|
uri = URI(@config.base_uri + path)
|
45
|
-
|
46
|
-
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v|
|
47
|
-
|
52
|
+
headers = {}
|
53
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
54
|
+
headers["Connection"] = "keep-alive"
|
48
55
|
cached = @cache.read(uri)
|
49
56
|
if !cached.nil?
|
50
|
-
|
57
|
+
headers["If-None-Match"] = cached.etag
|
51
58
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
59
|
+
response = @http_client.request("GET", uri, {
|
60
|
+
headers: headers
|
61
|
+
})
|
62
|
+
status = response.status.code
|
63
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers}\n\tbody: #{res.to_s}" }
|
64
|
+
# must fully read body for persistent connections
|
65
|
+
body = response.to_s
|
56
66
|
if status == 304 && !cached.nil?
|
57
67
|
body = cached.body
|
58
68
|
else
|
@@ -60,11 +70,11 @@ module LaunchDarkly
|
|
60
70
|
if status < 200 || status >= 300
|
61
71
|
raise UnexpectedResponseError.new(status)
|
62
72
|
end
|
63
|
-
body = fix_encoding(
|
64
|
-
etag =
|
73
|
+
body = fix_encoding(body, response.headers["content-type"])
|
74
|
+
etag = response.headers["etag"]
|
65
75
|
@cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
|
66
76
|
end
|
67
|
-
|
77
|
+
body
|
68
78
|
end
|
69
79
|
|
70
80
|
def fix_encoding(body, content_type)
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "json"
|
3
5
|
require "ld-eventsource"
|
@@ -44,7 +46,8 @@ module LaunchDarkly
|
|
44
46
|
opts = {
|
45
47
|
headers: headers,
|
46
48
|
read_timeout: READ_TIMEOUT_SECONDS,
|
47
|
-
logger: @config.logger
|
49
|
+
logger: @config.logger,
|
50
|
+
socket_factory: @config.socket_factory
|
48
51
|
}
|
49
52
|
log_connection_started
|
50
53
|
@es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
|
@@ -82,10 +85,8 @@ module LaunchDarkly
|
|
82
85
|
@config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
|
83
86
|
if method == PUT
|
84
87
|
message = JSON.parse(message.data, symbolize_names: true)
|
85
|
-
|
86
|
-
|
87
|
-
SEGMENTS => message[:data][:segments]
|
88
|
-
})
|
88
|
+
all_data = Impl::Model.make_all_store_data(message[:data])
|
89
|
+
@feature_store.init(all_data)
|
89
90
|
@initialized.make_true
|
90
91
|
@config.logger.info { "[LDClient] Stream initialized" }
|
91
92
|
@ready.set
|
@@ -94,7 +95,9 @@ module LaunchDarkly
|
|
94
95
|
for kind in [FEATURES, SEGMENTS]
|
95
96
|
key = key_for_path(kind, data[:path])
|
96
97
|
if key
|
97
|
-
|
98
|
+
data = data[:data]
|
99
|
+
Impl::Model.postprocess_item_after_deserializing!(kind, data)
|
100
|
+
@feature_store.upsert(kind, data)
|
98
101
|
break
|
99
102
|
end
|
100
103
|
end
|
data/lib/ldclient-rb/util.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require "net/http"
|
2
1
|
require "uri"
|
2
|
+
require "http"
|
3
3
|
|
4
4
|
module LaunchDarkly
|
5
5
|
# @private
|
@@ -18,14 +18,18 @@ module LaunchDarkly
|
|
18
18
|
end
|
19
19
|
ret
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def self.new_http_client(uri_s, config)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
http_client_options = {}
|
24
|
+
if config.socket_factory
|
25
|
+
http_client_options["socket_class"] = config.socket_factory
|
26
|
+
end
|
27
|
+
return HTTP::Client.new(http_client_options)
|
28
|
+
.timeout({
|
29
|
+
read: config.read_timeout,
|
30
|
+
connect: config.connect_timeout
|
31
|
+
})
|
32
|
+
.persistent(uri_s)
|
29
33
|
end
|
30
34
|
|
31
35
|
def self.log_exception(logger, message, exc)
|
data/lib/ldclient-rb/version.rb
CHANGED
@@ -0,0 +1,135 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
describe "EvaluationDetail" do
|
5
|
+
subject { EvaluationDetail }
|
6
|
+
|
7
|
+
it "sets properties" do
|
8
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).value).to eq "x"
|
9
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).variation_index).to eq 0
|
10
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off).reason).to eq EvaluationReason::off
|
11
|
+
end
|
12
|
+
|
13
|
+
it "checks parameter types" do
|
14
|
+
expect { EvaluationDetail.new(nil, nil, EvaluationReason::off) }.not_to raise_error
|
15
|
+
expect { EvaluationDetail.new(nil, 0, EvaluationReason::off) }.not_to raise_error
|
16
|
+
expect { EvaluationDetail.new(nil, "x", EvaluationReason::off) }.to raise_error(ArgumentError)
|
17
|
+
expect { EvaluationDetail.new(nil, 0, { kind: "OFF" }) }.to raise_error(ArgumentError)
|
18
|
+
expect { EvaluationDetail.new(nil, 0, nil) }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "equality test" do
|
22
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).to eq EvaluationDetail.new("x", 0, EvaluationReason::off)
|
23
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("y", 0, EvaluationReason::off)
|
24
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 1, EvaluationReason::off)
|
25
|
+
expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 0, EvaluationReason::fallthrough)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "EvaluationReason" do
|
30
|
+
subject { EvaluationReason }
|
31
|
+
|
32
|
+
values = [
|
33
|
+
[ EvaluationReason::off, EvaluationReason::OFF, { "kind" => "OFF" }, "OFF", nil ],
|
34
|
+
[ EvaluationReason::fallthrough, EvaluationReason::FALLTHROUGH,
|
35
|
+
{ "kind" => "FALLTHROUGH" }, "FALLTHROUGH", nil ],
|
36
|
+
[ EvaluationReason::target_match, EvaluationReason::TARGET_MATCH,
|
37
|
+
{ "kind" => "TARGET_MATCH" }, "TARGET_MATCH", nil ],
|
38
|
+
[ EvaluationReason::rule_match(1, "x"), EvaluationReason::RULE_MATCH,
|
39
|
+
{ "kind" => "RULE_MATCH", "ruleIndex" => 1, "ruleId" => "x" }, "RULE_MATCH(1,x)",
|
40
|
+
[ EvaluationReason::rule_match(2, "x"), EvaluationReason::rule_match(1, "y") ] ],
|
41
|
+
[ EvaluationReason::prerequisite_failed("x"), EvaluationReason::PREREQUISITE_FAILED,
|
42
|
+
{ "kind" => "PREREQUISITE_FAILED", "prerequisiteKey" => "x" }, "PREREQUISITE_FAILED(x)" ],
|
43
|
+
[ EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND), EvaluationReason::ERROR,
|
44
|
+
{ "kind" => "ERROR", "errorKind" => "FLAG_NOT_FOUND" }, "ERROR(FLAG_NOT_FOUND)" ]
|
45
|
+
]
|
46
|
+
values.each_index do |i|
|
47
|
+
params = values[i]
|
48
|
+
reason = params[0]
|
49
|
+
kind = params[1]
|
50
|
+
json_rep = params[2]
|
51
|
+
brief_str = params[3]
|
52
|
+
unequal_values = params[4]
|
53
|
+
|
54
|
+
describe "reason #{reason.kind}" do
|
55
|
+
it "has correct kind" do
|
56
|
+
expect(reason.kind).to eq kind
|
57
|
+
end
|
58
|
+
|
59
|
+
it "equality to self" do
|
60
|
+
expect(reason).to eq reason
|
61
|
+
end
|
62
|
+
|
63
|
+
it "inequality to others" do
|
64
|
+
values.each_index do |j|
|
65
|
+
if i != j
|
66
|
+
expect(reason).not_to eq values[j][0]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
if !unequal_values.nil?
|
70
|
+
unequal_values.each do |v|
|
71
|
+
expect(reason).not_to eq v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "JSON representation" do
|
77
|
+
expect(JSON.parse(reason.as_json.to_json)).to eq json_rep
|
78
|
+
expect(JSON.parse(reason.to_json)).to eq json_rep
|
79
|
+
end
|
80
|
+
|
81
|
+
it "brief representation" do
|
82
|
+
expect(reason.inspect).to eq brief_str
|
83
|
+
expect(reason.to_s).to eq brief_str
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it "reuses singleton reasons" do
|
89
|
+
expect(EvaluationReason::off).to be EvaluationReason::off
|
90
|
+
expect(EvaluationReason::fallthrough).to be EvaluationReason::fallthrough
|
91
|
+
expect(EvaluationReason::target_match).to be EvaluationReason::target_match
|
92
|
+
expect(EvaluationReason::rule_match(1, 'x')).not_to be EvaluationReason::rule_match(1, 'x')
|
93
|
+
expect(EvaluationReason::prerequisite_failed('x')).not_to be EvaluationReason::prerequisite_failed('x')
|
94
|
+
errors = [ EvaluationReason::ERROR_CLIENT_NOT_READY, EvaluationReason::ERROR_FLAG_NOT_FOUND,
|
95
|
+
EvaluationReason::ERROR_MALFORMED_FLAG, EvaluationReason::ERROR_USER_NOT_SPECIFIED, EvaluationReason::ERROR_EXCEPTION ]
|
96
|
+
errors.each do |e|
|
97
|
+
expect(EvaluationReason::error(e)).to be EvaluationReason::error(e)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "supports [] with JSON property names" do
|
102
|
+
expect(EvaluationReason::off[:kind]).to eq "OFF"
|
103
|
+
expect(EvaluationReason::off[:ruleIndex]).to be nil
|
104
|
+
expect(EvaluationReason::off[:ruleId]).to be nil
|
105
|
+
expect(EvaluationReason::off[:prerequisiteKey]).to be nil
|
106
|
+
expect(EvaluationReason::off[:errorKind]).to be nil
|
107
|
+
expect(EvaluationReason::rule_match(1, "x")[:ruleIndex]).to eq 1
|
108
|
+
expect(EvaluationReason::rule_match(1, "x")[:ruleId]).to eq "x"
|
109
|
+
expect(EvaluationReason::prerequisite_failed("x")[:prerequisiteKey]).to eq "x"
|
110
|
+
expect(EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND)[:errorKind]).to eq "FLAG_NOT_FOUND"
|
111
|
+
end
|
112
|
+
|
113
|
+
it "freezes string properties" do
|
114
|
+
rm = EvaluationReason::rule_match(1, "x")
|
115
|
+
expect { rm.rule_id.upcase! }.to raise_error(RuntimeError)
|
116
|
+
pf = EvaluationReason::prerequisite_failed("x")
|
117
|
+
expect { pf.prerequisite_key.upcase! }.to raise_error(RuntimeError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "checks parameter types" do
|
121
|
+
expect { EvaluationReason::rule_match(nil, "x") }.to raise_error(ArgumentError)
|
122
|
+
expect { EvaluationReason::rule_match(true, "x") }.to raise_error(ArgumentError)
|
123
|
+
expect { EvaluationReason::rule_match(1, nil) }.not_to raise_error # we allow nil rule_id for backward compatibility
|
124
|
+
expect { EvaluationReason::rule_match(1, 9) }.to raise_error(ArgumentError)
|
125
|
+
expect { EvaluationReason::prerequisite_failed(nil) }.to raise_error(ArgumentError)
|
126
|
+
expect { EvaluationReason::prerequisite_failed(9) }.to raise_error(ArgumentError)
|
127
|
+
expect { EvaluationReason::error(nil) }.to raise_error(ArgumentError)
|
128
|
+
expect { EvaluationReason::error(9) }.to raise_error(ArgumentError)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "does not allow direct access to constructor" do
|
132
|
+
expect { EvaluationReason.new(:off, nil, nil, nil, nil) }.to raise_error(NoMethodError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|