absmartly-sdk 0.1.2

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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +180 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/Gemfile +21 -0
  8. data/Gemfile.lock +92 -0
  9. data/LICENSE.txt +21 -0
  10. data/README +180 -0
  11. data/Rakefile +12 -0
  12. data/example/example.rb +48 -0
  13. data/lib/a_b_smartly.rb +92 -0
  14. data/lib/a_b_smartly_config.rb +49 -0
  15. data/lib/absmartly/md5.rb +178 -0
  16. data/lib/absmartly/variant_assigner.rb +36 -0
  17. data/lib/absmartly/version.rb +5 -0
  18. data/lib/absmartly.rb +57 -0
  19. data/lib/audience_deserializer.rb +8 -0
  20. data/lib/audience_matcher.rb +38 -0
  21. data/lib/client.rb +80 -0
  22. data/lib/client_config.rb +43 -0
  23. data/lib/context.rb +528 -0
  24. data/lib/context_config.rb +70 -0
  25. data/lib/context_data_deserializer.rb +8 -0
  26. data/lib/context_data_provider.rb +8 -0
  27. data/lib/context_event_handler.rb +8 -0
  28. data/lib/context_event_logger.rb +9 -0
  29. data/lib/context_event_serializer.rb +8 -0
  30. data/lib/default_audience_deserializer.rb +13 -0
  31. data/lib/default_context_data_deserializer.rb +16 -0
  32. data/lib/default_context_data_provider.rb +15 -0
  33. data/lib/default_context_event_handler.rb +15 -0
  34. data/lib/default_context_event_serializer.rb +58 -0
  35. data/lib/default_http_client.rb +61 -0
  36. data/lib/default_http_client_config.rb +19 -0
  37. data/lib/default_variable_parser.rb +13 -0
  38. data/lib/hashing.rb +9 -0
  39. data/lib/http_client.rb +20 -0
  40. data/lib/json/attribute.rb +30 -0
  41. data/lib/json/context_data.rb +31 -0
  42. data/lib/json/experiment.rb +83 -0
  43. data/lib/json/experiment_application.rb +27 -0
  44. data/lib/json/experiment_variant.rb +29 -0
  45. data/lib/json/exposure.rb +60 -0
  46. data/lib/json/goal_achievement.rb +32 -0
  47. data/lib/json/publish_event.rb +42 -0
  48. data/lib/json/unit.rb +30 -0
  49. data/lib/json_expr/evaluator.rb +27 -0
  50. data/lib/json_expr/expr_evaluator.rb +122 -0
  51. data/lib/json_expr/json_expr.rb +37 -0
  52. data/lib/json_expr/operator.rb +8 -0
  53. data/lib/json_expr/operators/and_combinator.rb +14 -0
  54. data/lib/json_expr/operators/binary_operator.rb +22 -0
  55. data/lib/json_expr/operators/boolean_combinator.rb +15 -0
  56. data/lib/json_expr/operators/equals_operator.rb +12 -0
  57. data/lib/json_expr/operators/greater_than_operator.rb +12 -0
  58. data/lib/json_expr/operators/greater_than_or_equal_operator.rb +12 -0
  59. data/lib/json_expr/operators/in_operator.rb +23 -0
  60. data/lib/json_expr/operators/less_than_operator.rb +12 -0
  61. data/lib/json_expr/operators/less_than_or_equal_operator.rb +12 -0
  62. data/lib/json_expr/operators/match_operator.rb +17 -0
  63. data/lib/json_expr/operators/nil_operator.rb +11 -0
  64. data/lib/json_expr/operators/not_operator.rb +11 -0
  65. data/lib/json_expr/operators/or_combinator.rb +14 -0
  66. data/lib/json_expr/operators/unary_operator.rb +13 -0
  67. data/lib/json_expr/operators/value_operator.rb +11 -0
  68. data/lib/json_expr/operators/var_operator.rb +21 -0
  69. data/lib/scheduled_executor_service.rb +8 -0
  70. data/lib/scheduled_thread_pool_executor.rb +14 -0
  71. data/lib/string.rb +30 -0
  72. data/lib/variable_parser.rb +8 -0
  73. data/lib/variant_assigner.rb +47 -0
  74. data/sig/absmartly.rbs +4 -0
  75. metadata +120 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "context_event_serializer"
4
+
5
+ class DefaultContextEventSerializer < ContextEventSerializer
6
+ def serialize(event)
7
+ units = event.units.nil? ? [] : event.units.map do |unit|
8
+ {
9
+ type: unit.type,
10
+ uid: unit.uid,
11
+ }
12
+ end
13
+ req = {}
14
+ unless units.empty?
15
+ req = {
16
+ publishedAt: event.published_at,
17
+ units: units,
18
+ hashed: event.hashed
19
+ }
20
+ end
21
+
22
+ req[:goals] = event.goals.map do |x|
23
+ {
24
+ name: x.name,
25
+ achievedAt: x.achieved_at,
26
+ properties: x.properties,
27
+ }
28
+ end unless event.goals.nil?
29
+
30
+ req[:exposures] = event.exposures.select { |x| !x.id.nil? }.map do |x|
31
+ {
32
+ id: x.id,
33
+ name: x.name,
34
+ unit: x.unit,
35
+ exposedAt: x.exposed_at.to_i,
36
+ variant: x.variant,
37
+ assigned: x.assigned,
38
+ eligible: x.eligible,
39
+ overridden: x.overridden,
40
+ fullOn: x.full_on,
41
+ custom: x.custom,
42
+ audienceMismatch: x.audience_mismatch
43
+ }
44
+ end unless event.exposures.nil?
45
+
46
+ req[:attributes] = event.attributes.map do |x|
47
+ {
48
+ name: x.name,
49
+ value: x.value,
50
+ setAt: x.set_at,
51
+ }
52
+ end unless event.attributes.nil?
53
+
54
+ return nil if req.empty?
55
+
56
+ req.to_json
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "uri"
5
+ require_relative "http_client"
6
+
7
+ class DefaultHttpClient < HttpClient
8
+ attr_accessor :config, :session
9
+
10
+ def self.create(config)
11
+ DefaultHttpClient.new(config)
12
+ end
13
+
14
+ def initialize(config)
15
+ @config = config
16
+ @session = Faraday.new("") do |f|
17
+ f.request :retry,
18
+ max: config.max_retries,
19
+ interval: config.retry_interval,
20
+ interval_randomness: 0.5,
21
+ backoff_factor: 2
22
+ f.options.timeout = config.connect_timeout
23
+ f.options.open_timeout = config.connection_request_timeout
24
+ end
25
+ end
26
+
27
+ # def context_data
28
+ # end
29
+
30
+ def get(url, query, headers)
31
+ @session.get(url, query, headers)
32
+ end
33
+
34
+ def put(url, query, headers, body)
35
+ @session.put(add_tracking(url, query), body, headers)
36
+ end
37
+
38
+ def post(url, query, headers, body)
39
+ @session.post(add_tracking(url, query), body, headers)
40
+ end
41
+
42
+ def close
43
+ @session.close
44
+ end
45
+
46
+ def self.default_response(status_code, status_message, content_type, content)
47
+ env = Faraday::Env.from(status: status_code, body: content || status_message,
48
+ response_headers: { "Content-Type" => content_type })
49
+ Faraday::Response.new(env)
50
+ end
51
+
52
+ private
53
+ def add_tracking(url, params)
54
+ parsed = URI.parse(url)
55
+ query = parsed.query ? CGI.parse(parsed.query) : {}
56
+ query = query.merge(params) if params && params.is_a?(Hash)
57
+ parsed.query = URI.encode_www_form(query)
58
+ str = parsed.to_s
59
+ str[-1] == "?" ? str.chop : str
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DefaultHttpClientConfig
4
+ attr_accessor :connect_timeout,
5
+ :connection_request_timeout,
6
+ :retry_interval,
7
+ :max_retries
8
+
9
+ def self.create
10
+ DefaultHttpClientConfig.new
11
+ end
12
+
13
+ def initialize
14
+ @connect_timeout = 3.0
15
+ @connection_request_timeout = 3.0
16
+ @retry_interval = 0.5
17
+ @max_retries = 5
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "variable_parser"
4
+
5
+ class DefaultVariableParser < VariableParser
6
+ attr_accessor :reader, :log
7
+
8
+ def parse(context, experiment_name, variant_name, config)
9
+ JSON.parse(config, symbolize_names: true)
10
+ rescue JSON::ParserError
11
+ nil
12
+ end
13
+ end
data/lib/hashing.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ class Hashing
6
+ def self.hash_unit(str)
7
+ Digest::MD5.base64digest(str.to_s).gsub("==", "").gsub("+", "-").gsub("/", "_")
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HttpClient
4
+ # @interface method
5
+ def response
6
+ raise NotImplementedError.new("You must implement response method.")
7
+ end
8
+
9
+ def get(url, query, headers)
10
+ raise NotImplementedError.new("You must implement get method.")
11
+ end
12
+
13
+ def post(url, query, headers, body)
14
+ raise NotImplementedError.new("You must implement post method.")
15
+ end
16
+
17
+ def put(url, query, headers, body)
18
+ raise NotImplementedError.new("You must implement put method.")
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Attribute
4
+ attr_accessor :name, :value, :set_at
5
+
6
+ def initialize(name = nil, value = nil, set_at = nil)
7
+ @name = name
8
+ @value = value
9
+ @set_at = set_at
10
+ end
11
+
12
+ def ==(o)
13
+ return true if self.object_id == o.object_id
14
+ return false if o.nil? || self.class != o.class
15
+
16
+ @name == o.name && @value == o.value && @set_at == o.set_at
17
+ end
18
+
19
+ def hash_code
20
+ { name: @name, value: @value, set_at: @set_at }
21
+ end
22
+
23
+ def to_s
24
+ "Attribute{" +
25
+ "name='" + @name + "'" +
26
+ ", value=" + @value +
27
+ ", setAt=" + @set_at +
28
+ "}"
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "experiment"
4
+
5
+ class ContextData
6
+ attr_accessor :experiments
7
+
8
+ def initialize(experiments = [])
9
+ @experiments = experiments.map do |experiment|
10
+ Experiment.new(experiment)
11
+ end unless experiments.nil?
12
+ self
13
+ end
14
+
15
+ def ==(o)
16
+ return true if self.object_id == o.object_id
17
+ return false if o.nil? || self.class != o.class
18
+
19
+ @experiments == o.experiments
20
+ end
21
+
22
+ def hash_code
23
+ { name: @name, config: @config }
24
+ end
25
+
26
+ def to_s
27
+ "ContextData{" +
28
+ "experiments='" + @experiments.join +
29
+ "}"
30
+ end
31
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../string"
4
+ require_relative "experiment_application"
5
+ require_relative "experiment_variant"
6
+
7
+ class Experiment
8
+ attr_accessor :id, :name, :unit_type, :iteration, :seed_hi, :seed_lo, :split,
9
+ :traffic_seed_hi, :traffic_seed_lo, :traffic_split, :full_on_variant,
10
+ :applications, :variants, :audience_strict, :audience
11
+
12
+ def initialize(args = {})
13
+ args.each do |name, value|
14
+ if name == :applications
15
+ @applications = assign_to_klass(ExperimentApplication, value)
16
+ elsif name == :variants
17
+ @variants = assign_to_klass(ExperimentVariant, value)
18
+ else
19
+ self.instance_variable_set("@#{name.to_s.underscore}", value)
20
+ end
21
+ end
22
+ @audience_strict ||= false
23
+ self
24
+ end
25
+
26
+ def assign_to_klass(klass, arr)
27
+ arr.map do |item|
28
+ return item if item.is_a?(klass)
29
+
30
+ klass.new(*item.values)
31
+ end
32
+ end
33
+
34
+ def ==(o)
35
+ return true if self.object_id == o.object_id
36
+ return false if o.nil? || self.class != o.class
37
+
38
+ that = o
39
+ @id == that.id && @iteration == that.iteration && @seed_hi == that.seed_hi && @seed_lo == that.seed_lo &&
40
+ @traffic_seed_hi == that.traffic_seed_hi && @traffic_seed_lo == that.traffic_seed_lo &&
41
+ @full_on_variant == that.full_on_variant && @name == that.name &&
42
+ @unit_type == that.unit_type && @split == that.split &&
43
+ @traffic_split == that.traffic_split && @applications == that.applications &&
44
+ @variants == that.variants && @audience_strict == that.audience_strict &&
45
+ @audience == that.audience
46
+ end
47
+
48
+ def hash_code
49
+ {
50
+ id: @id,
51
+ name: @name,
52
+ unit_type: @unit_type,
53
+ iteration: @iteration,
54
+ seed_hi: @seed_hi,
55
+ seed_lo: @seed_lo,
56
+ traffic_seed_hi: @traffic_seed_hi,
57
+ traffic_seed_lo: @traffic_seed_lo,
58
+ full_on_variant: @full_on_variant,
59
+ audience_strict: @audience_strict,
60
+ audience: @audience
61
+ }
62
+ end
63
+
64
+ def to_s
65
+ "ContextExperiment{" +
66
+ "id=" + @id +
67
+ ", name='" + @name + "'" +
68
+ ", unitType='" + @unit_type + "'" +
69
+ ", iteration=" + @iteration +
70
+ ", seedHi=" + @seed_hi +
71
+ ", seedLo=" + @seed_lo +
72
+ ", split=" + @split.join +
73
+ ", trafficSeedHi=" + @traffic_seed_hi +
74
+ ", trafficSeedLo=" + @traffic_seed_lo +
75
+ ", trafficSplit=" + @traffic_split.join +
76
+ ", fullOnVariant=" + @full_on_variant +
77
+ ", applications=" + @applications.join +
78
+ ", variants=" + @variants.join +
79
+ ", audienceStrict=" + @audience_strict +
80
+ ", audience='" + @audience + "'" +
81
+ "}"
82
+ end
83
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ExperimentApplication
4
+ attr_accessor :name
5
+
6
+ def initialize(name = nil)
7
+ @name = name
8
+ end
9
+
10
+ def ==(o)
11
+ return true if self.object_id == o.object_id
12
+ return false if o.nil? || self.class != o.class
13
+
14
+ that = o
15
+ @name == that.name
16
+ end
17
+
18
+ def hash_code
19
+ { name: @name }
20
+ end
21
+
22
+ def to_s
23
+ "ExperimentApplication{" +
24
+ "name='" + @name + "'" +
25
+ "}"
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ExperimentVariant
4
+ attr_accessor :name, :config
5
+
6
+ def initialize(name = nil, config)
7
+ @name = name
8
+ @config = config
9
+ end
10
+
11
+ def ==(o)
12
+ return true if self.object_id == o.object_id
13
+ return false if o.nil? || self.class != o.class
14
+
15
+ that = o
16
+ @name == that.name && @config == that.config
17
+ end
18
+
19
+ def hash_code
20
+ { name: @name, config: @config }
21
+ end
22
+
23
+ def to_s
24
+ "ExperimentVariant{" +
25
+ "name='" + @name + "'" +
26
+ ", config='" + @config + "'" +
27
+ "}"
28
+ end
29
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Exposure
4
+ attr_accessor :id, :name, :unit, :variant, :exposed_at, :assigned, :eligible,
5
+ :overridden, :full_on, :custom, :audience_mismatch
6
+
7
+ def initialize(id = nil, name = nil, unit = nil, variant = nil,
8
+ exposed_at = nil, assigned = nil, eligible = nil,
9
+ overridden = nil, full_on = nil, custom = nil,
10
+ audience_mismatch = nil)
11
+ @id = id
12
+ @name = name
13
+ @unit = unit
14
+ @variant = variant
15
+ @exposed_at = exposed_at
16
+ @assigned = assigned
17
+ @eligible = eligible
18
+ @overridden = overridden
19
+ @full_on = full_on
20
+ @custom = custom
21
+ @audience_mismatch = audience_mismatch
22
+ end
23
+
24
+ def ==(o)
25
+ return true if self.object_id == o.object_id
26
+ return false if o.nil? || self.class != o.class
27
+
28
+ @id == o.id && @name == o.name && @unit == o.unit &&
29
+ @variant == o.variant && @exposed_at == o.exposed_at &&
30
+ @assigned == o.assigned && @eligible == o.eligible &&
31
+ @overridden == o.overridden && @full_on == o.full_on &&
32
+ @custom == o.custom && @audience_mismatch == o.audience_mismatch
33
+ end
34
+
35
+ def hash_code
36
+ {
37
+ id: @id, name: @name, unit: @unit,
38
+ variant: @variant, exposed_at: @exposed_at,
39
+ assigned: @assigned, eligible: @eligible,
40
+ overridden: @overridden, full_on: @full_on,
41
+ custom: @custom, audience_mismatch: @audience_mismatch
42
+ }
43
+ end
44
+
45
+ def to_s
46
+ "Exposure{" +
47
+ "id=" + @id +
48
+ "name='" + @name + "'" +
49
+ ", unit=" + @unit +
50
+ ", variant=" + @variant +
51
+ ", exposed_at=" + @exposed_at +
52
+ ", assigned=" + @assigned +
53
+ ", eligible=" + @eligible +
54
+ ", overridden=" + @overridden +
55
+ ", full_on=" + @full_on +
56
+ ", custom=" + @custom +
57
+ ", audience_mismatch=" + @audience_mismatch +
58
+ "}"
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GoalAchievement
4
+ attr_accessor :name, :achieved_at, :properties
5
+
6
+ def initialize(name = nil, achieved_at = nil, properties = nil)
7
+ @name = name
8
+ @achieved_at = achieved_at
9
+ @properties = properties
10
+ end
11
+
12
+ def ==(o)
13
+ return true if self.object_id == o.object_id
14
+ return false if o.nil? || self.class != o.class
15
+
16
+ that = o
17
+ @name == that.name && @achieved_at == that.achieved_at &&
18
+ @properties == that.properties
19
+ end
20
+
21
+ def hash_code
22
+ { name: @name, achieved_at: @achieved_at, properties: @properties }
23
+ end
24
+
25
+ def to_s
26
+ "GoalAchievement{" +
27
+ "name='" + @name + "'" +
28
+ ", achieved_at='" + @achieved_at + "'" +
29
+ ", properties='" + @properties.inspect + "'" +
30
+ "}"
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PublishEvent
4
+ attr_accessor :hashed, :units, :published_at, :exposures, :goals, :attributes
5
+
6
+ def initialize
7
+ @published_at = 0
8
+ @hashed = false
9
+ end
10
+
11
+ def ==(o)
12
+ return true if self.object_id == o.object_id
13
+ return false if o.nil? || self.class != o.class
14
+
15
+ that = o
16
+ @hashed == that.hashed && @units == that.units &&
17
+ @published_at == that.published_at && @exposures == that.exposures &&
18
+ @goals == that.goals && @attributes == that.attributes
19
+ end
20
+
21
+ def hash_code
22
+ {
23
+ hashed: @hashed,
24
+ units: @units,
25
+ published_at: @published_at,
26
+ exposures: @exposures,
27
+ goals: @goals,
28
+ attributes: @attributes
29
+ }
30
+ end
31
+
32
+ def to_s
33
+ "PublishEvent{" +
34
+ "hashedUnits=" + @hashed +
35
+ ", units=" + @units.inspect +
36
+ ", publishedAt=" + @published_at +
37
+ ", exposures=" + @exposures.inspect +
38
+ ", goals=" + @goals.inspect +
39
+ ", attributes=" + @attributes.join +
40
+ "}"
41
+ end
42
+ end
data/lib/json/unit.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Unit
4
+ attr_accessor :type, :uid
5
+
6
+ def initialize(type = nil, uid = nil)
7
+ @type = type
8
+ @uid = uid
9
+ end
10
+
11
+ def ==(o)
12
+ return true if self.object_id == o.object_id
13
+ return false if o.nil? || self.class != o.class
14
+
15
+ @type == o.type && @uid == o.uid
16
+ end
17
+
18
+ def hash_code
19
+ {
20
+ type: @type, uid: @uid
21
+ }
22
+ end
23
+
24
+ def to_s
25
+ "Unit{" +
26
+ "type='" + @type + "'" +
27
+ ", uid=" + @uid +
28
+ "}"
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Evaluator
4
+ def evaluate(expr)
5
+ raise NotImplementedError.new("You must implement evaluate method.")
6
+ end
7
+
8
+ def boolean_convert(_)
9
+ raise NotImplementedError.new("You must implement boolean convert method.")
10
+ end
11
+
12
+ def number_convert(_)
13
+ raise NotImplementedError.new("You must implement number convert method.")
14
+ end
15
+
16
+ def string_convert(_)
17
+ raise NotImplementedError.new("You must implement string convert method.")
18
+ end
19
+
20
+ def extract_var(_)
21
+ raise NotImplementedError.new("You must implement extract var method.")
22
+ end
23
+
24
+ def compare(_, _)
25
+ raise NotImplementedError.new("You must implement extract_var method.")
26
+ end
27
+ end