launchdarkly-server-sdk 5.8.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +28 -122
  3. data/.ldrelease/circleci/linux/execute.sh +18 -0
  4. data/.ldrelease/circleci/mac/execute.sh +18 -0
  5. data/.ldrelease/circleci/template/build.sh +29 -0
  6. data/.ldrelease/circleci/template/publish.sh +23 -0
  7. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  8. data/.ldrelease/circleci/template/test.sh +10 -0
  9. data/.ldrelease/circleci/template/update-version.sh +8 -0
  10. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  11. data/.ldrelease/config.yml +7 -3
  12. data/CHANGELOG.md +9 -0
  13. data/CONTRIBUTING.md +1 -1
  14. data/Gemfile.lock +69 -42
  15. data/README.md +2 -2
  16. data/azure-pipelines.yml +1 -1
  17. data/launchdarkly-server-sdk.gemspec +16 -16
  18. data/lib/ldclient-rb.rb +0 -1
  19. data/lib/ldclient-rb/config.rb +15 -3
  20. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  21. data/lib/ldclient-rb/events.rb +1 -4
  22. data/lib/ldclient-rb/file_data_source.rb +1 -1
  23. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  24. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  25. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  26. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  27. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  28. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  29. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
  30. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  32. data/lib/ldclient-rb/ldclient.rb +14 -9
  33. data/lib/ldclient-rb/polling.rb +1 -4
  34. data/lib/ldclient-rb/requestor.rb +25 -15
  35. data/lib/ldclient-rb/stream.rb +9 -6
  36. data/lib/ldclient-rb/util.rb +12 -8
  37. data/lib/ldclient-rb/version.rb +1 -1
  38. data/spec/evaluation_detail_spec.rb +135 -0
  39. data/spec/event_sender_spec.rb +20 -2
  40. data/spec/http_util.rb +11 -1
  41. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  42. data/spec/impl/evaluator_clause_spec.rb +55 -0
  43. data/spec/impl/evaluator_operators_spec.rb +141 -0
  44. data/spec/impl/evaluator_rule_spec.rb +96 -0
  45. data/spec/impl/evaluator_segment_spec.rb +125 -0
  46. data/spec/impl/evaluator_spec.rb +305 -0
  47. data/spec/impl/evaluator_spec_base.rb +75 -0
  48. data/spec/impl/model/serialization_spec.rb +41 -0
  49. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  50. data/spec/ldclient_end_to_end_spec.rb +34 -0
  51. data/spec/ldclient_spec.rb +10 -8
  52. data/spec/polling_spec.rb +2 -2
  53. data/spec/redis_feature_store_spec.rb +2 -2
  54. data/spec/requestor_spec.rb +11 -11
  55. metadata +89 -46
  56. data/lib/ldclient-rb/evaluation.rb +0 -462
  57. 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
@@ -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, @store, @config.logger, @event_factory_default)
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 ? { kind: 'ERROR', errorKind: 'EXCEPTION' } : nil, details_only_if_tracked)
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('CLIENT_NOT_READY', default)
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('CLIENT_NOT_READY', default)
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('FLAG_NOT_FOUND', default)
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('USER_NOT_SPECIFIED', default)
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, @store, @config.logger, event_factory)
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('EXCEPTION', default)
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
@@ -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
- @client = Util.new_http_client(@config.base_uri, @config)
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
- @client.finish
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
- req = Net::HTTP::Get.new(uri)
46
- Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
47
- req["Connection"] = "keep-alive"
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
- req["If-None-Match"] = cached.etag
57
+ headers["If-None-Match"] = cached.etag
51
58
  end
52
- res = @client.request(req)
53
- status = res.code.to_i
54
- @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }
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(res.body, res["content-type"])
64
- etag = res["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
- JSON.parse(body, symbolize_names: true)
77
+ body
68
78
  end
69
79
 
70
80
  def fix_encoding(body, content_type)
@@ -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
- @feature_store.init({
86
- FEATURES => message[:data][:flags],
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
- @feature_store.upsert(kind, data[:data])
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
@@ -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
- uri = URI(uri_s)
24
- client = Net::HTTP.new(uri.hostname, uri.port)
25
- client.use_ssl = true if uri.scheme == "https"
26
- client.open_timeout = config.connect_timeout
27
- client.read_timeout = config.read_timeout
28
- client
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)
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.8.2"
2
+ VERSION = "6.0.0"
3
3
  end
@@ -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