absmartly-sdk 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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