flagsmith 2.0.0 → 3.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/.rspec +2 -0
- data/.rubocop.yml +4 -1
- data/.rubocop_todo.yml +0 -1
- data/.ruby-version +1 -0
- data/Gemfile.lock +35 -6
- data/LICENCE +4 -5
- data/README.md +8 -64
- data/Rakefile +5 -1
- data/example/.env.development +5 -0
- data/example/.env.test +4 -0
- data/example/.gitignore +5 -0
- data/example/.hanamirc +3 -0
- data/example/.rspec +2 -0
- data/example/Gemfile +25 -0
- data/example/Gemfile.lock +269 -0
- data/example/README.md +29 -0
- data/example/Rakefile +9 -0
- data/example/apps/web/application.rb +162 -0
- data/example/apps/web/config/routes.rb +1 -0
- data/example/apps/web/controllers/.gitkeep +0 -0
- data/example/apps/web/controllers/home/index.rb +32 -0
- data/example/apps/web/templates/application.html.slim +7 -0
- data/example/apps/web/templates/home/index.html.slim +10 -0
- data/example/apps/web/views/application_layout.rb +7 -0
- data/example/apps/web/views/home/index.rb +29 -0
- data/example/config/boot.rb +2 -0
- data/example/config/environment.rb +17 -0
- data/example/config/initializers/.gitkeep +0 -0
- data/example/config/initializers/flagsmith.rb +9 -0
- data/example/config/puma.rb +15 -0
- data/example/config.ru +3 -0
- data/example/spec/example/entities/.gitkeep +0 -0
- data/example/spec/example/mailers/.gitkeep +0 -0
- data/example/spec/example/repositories/.gitkeep +0 -0
- data/example/spec/features_helper.rb +12 -0
- data/example/spec/spec_helper.rb +103 -0
- data/example/spec/support/.gitkeep +0 -0
- data/example/spec/support/capybara.rb +8 -0
- data/example/spec/web/controllers/.gitkeep +0 -0
- data/example/spec/web/controllers/home/index_spec.rb +9 -0
- data/example/spec/web/features/.gitkeep +0 -0
- data/example/spec/web/views/application_layout_spec.rb +10 -0
- data/example/spec/web/views/home/index_spec.rb +10 -0
- data/lib/flagsmith/engine/core.rb +88 -0
- data/lib/flagsmith/engine/environments/models.rb +61 -0
- data/lib/flagsmith/engine/features/models.rb +173 -0
- data/lib/flagsmith/engine/identities/models.rb +115 -0
- data/lib/flagsmith/engine/organisations/models.rb +28 -0
- data/lib/flagsmith/engine/projects/models.rb +31 -0
- data/lib/flagsmith/engine/segments/constants.rb +41 -0
- data/lib/flagsmith/engine/segments/evaluator.rb +68 -0
- data/lib/flagsmith/engine/segments/models.rb +121 -0
- data/lib/flagsmith/engine/utils/hash_func.rb +34 -0
- data/lib/flagsmith/hash_slice.rb +12 -0
- data/lib/flagsmith/sdk/analytics_processor.rb +39 -0
- data/lib/flagsmith/sdk/api_client.rb +47 -0
- data/lib/flagsmith/sdk/config.rb +91 -0
- data/lib/flagsmith/sdk/errors.rb +9 -0
- data/lib/flagsmith/sdk/instance_methods.rb +137 -0
- data/lib/flagsmith/sdk/intervals.rb +24 -0
- data/lib/flagsmith/sdk/models/flag.rb +62 -0
- data/lib/flagsmith/sdk/models/flags/collection.rb +105 -0
- data/lib/flagsmith/sdk/pooling_manager.rb +31 -0
- data/lib/flagsmith/version.rb +5 -0
- data/lib/flagsmith.rb +79 -101
- metadata +104 -6
- data/.gitignore +0 -57
- data/flagsmith.gemspec +0 -22
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../utils/hash_func'
|
4
|
+
|
5
|
+
module Flagsmith
|
6
|
+
module Engine
|
7
|
+
# FeatureStateModel
|
8
|
+
class FeatureState
|
9
|
+
include Flagsmith::Engine::Utils::HashFunc
|
10
|
+
|
11
|
+
attr_reader :feature, :enabled, :django_id, :uuid, :feature_segment
|
12
|
+
attr_accessor :multivariate_feature_state_values
|
13
|
+
|
14
|
+
def initialize(params = {})
|
15
|
+
@feature = params.fetch(:feature)
|
16
|
+
@enabled = params.fetch(:enabled)
|
17
|
+
@feature_segment = params.fetch(:feature_segment, nil)
|
18
|
+
@django_id = params.fetch(:django_id, nil)
|
19
|
+
@feature_state_value = params.fetch(:feature_state_value, nil)
|
20
|
+
@uuid = params.fetch(:uuid, SecureRandom.uuid)
|
21
|
+
@multivariate_feature_state_values = params.fetch(:multivariate_feature_state_values, [])
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_writer :feature_state_value
|
25
|
+
|
26
|
+
def get_value(identity_id = nil)
|
27
|
+
return multivariate_value(identity_id) if identity_id && multivariate_feature_state_values.length.positive?
|
28
|
+
|
29
|
+
@feature_state_value
|
30
|
+
end
|
31
|
+
|
32
|
+
alias set_value feature_state_value=
|
33
|
+
alias feature_state_uuid uuid
|
34
|
+
alias enabled? enabled
|
35
|
+
|
36
|
+
def multivariate_value(identity_id)
|
37
|
+
percentage_value = hashed_percentage_for_object_ids([django_id || uuid, identity_id])
|
38
|
+
|
39
|
+
start_percentage = 0
|
40
|
+
multivariate_feature_state_values.sort.each do |multi_fs_value|
|
41
|
+
limit = multi_fs_value.percentage_allocation + start_percentage
|
42
|
+
|
43
|
+
if start_percentage <= percentage_value && percentage_value < limit
|
44
|
+
return multi_fs_value.multivariate_feature_option.value
|
45
|
+
end
|
46
|
+
|
47
|
+
start_percentage = limit
|
48
|
+
end
|
49
|
+
@feature_state_value
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns `true` if `self` is higher segment priority than `other`
|
53
|
+
# (i.e. has lower value for feature_segment.priority)
|
54
|
+
# NOTE:
|
55
|
+
# A segment will be considered higher priority only if:
|
56
|
+
# 1. `other` does not have a feature segment
|
57
|
+
# (i.e: it is an environment feature state or it's a
|
58
|
+
# feature state with feature segment but from an old document
|
59
|
+
# that does not have `feature_segment.priority`)
|
60
|
+
# but `self` does.
|
61
|
+
# 2. `other` have a feature segment with high priority
|
62
|
+
def higher_segment_priority?(other)
|
63
|
+
feature_segment.priority.to_i < (other&.feature_segment&.priority || Float::INFINITY)
|
64
|
+
rescue TypeError, NoMethodError
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
class << self
|
69
|
+
def build(json)
|
70
|
+
multivariate_feature_state_values = build_multivariate_values(json[:multivariate_feature_state_values])
|
71
|
+
attributes = json.slice(:uuid, :enabled, :django_id, :feature_state_value)
|
72
|
+
.merge(feature: Flagsmith::Engine::Feature.build(json[:feature]))
|
73
|
+
.merge(multivariate_feature_state_values: multivariate_feature_state_values)
|
74
|
+
if json.key?(:feature_segment) && !json[:feature_segment].nil?
|
75
|
+
attributes = attributes.merge(
|
76
|
+
feature_segment: Flagsmith::Engine::Features::Segment.new(json[:feature_segment])
|
77
|
+
)
|
78
|
+
end
|
79
|
+
new(**attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_multivariate_values(multivariate_feature_state_values)
|
83
|
+
return [] unless multivariate_feature_state_values&.any?
|
84
|
+
|
85
|
+
multivariate_feature_state_values.map do |fsv|
|
86
|
+
Flagsmith::Engine::Features::MultivariateStateValue.build(fsv)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# FeatureModel
|
93
|
+
class Feature
|
94
|
+
attr_reader :id, :name, :type
|
95
|
+
|
96
|
+
def initialize(id:, name:, type:)
|
97
|
+
@id = id
|
98
|
+
@name = name
|
99
|
+
@type = type
|
100
|
+
end
|
101
|
+
|
102
|
+
def ==(other)
|
103
|
+
return false if other.nil?
|
104
|
+
|
105
|
+
id == other.id
|
106
|
+
end
|
107
|
+
|
108
|
+
class << self
|
109
|
+
def build(json)
|
110
|
+
new(**json.slice(:id, :name, :type))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module Features
|
116
|
+
# MultivariateFeatureOptionModel
|
117
|
+
class MultivariateOption
|
118
|
+
attr_reader :value, :id
|
119
|
+
|
120
|
+
def initialize(value:, id: nil)
|
121
|
+
@value = value
|
122
|
+
@id = id
|
123
|
+
end
|
124
|
+
|
125
|
+
class << self
|
126
|
+
def build(json)
|
127
|
+
new(**json.slice(:id, :value))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# MultivariateFeatureStateValueModel
|
133
|
+
class MultivariateStateValue
|
134
|
+
attr_reader :id, :multivariate_feature_option, :percentage_allocation, :mv_fs_value_uuid
|
135
|
+
|
136
|
+
def initialize(id:, multivariate_feature_option:, percentage_allocation:, mv_fs_value_uuid: SecureRandom.uuid)
|
137
|
+
@id = id
|
138
|
+
@percentage_allocation = percentage_allocation
|
139
|
+
@multivariate_feature_option = multivariate_feature_option
|
140
|
+
@mv_fs_value_uuid = mv_fs_value_uuid
|
141
|
+
end
|
142
|
+
|
143
|
+
def <=>(other)
|
144
|
+
return false if other.nil?
|
145
|
+
|
146
|
+
if !id.nil? && !other.id.nil?
|
147
|
+
id - other.id
|
148
|
+
else
|
149
|
+
mv_fs_value_uuid <=> other.mv_fs_value_uuid
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class << self
|
154
|
+
def build(json)
|
155
|
+
new(
|
156
|
+
**json.slice(:id, :percentage_allocation, :mv_fs_value_uuid)
|
157
|
+
.merge(multivariate_feature_option: MultivariateOption.build(json[:multivariate_feature_option]))
|
158
|
+
)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# FeatureSegment
|
164
|
+
class Segment
|
165
|
+
attr_reader :priority
|
166
|
+
|
167
|
+
def initialize(params = {})
|
168
|
+
@priority = params[:priority].nil? ? nil : params[:priority].to_i
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flagsmith
|
4
|
+
module Engine
|
5
|
+
# IdentityModel
|
6
|
+
class Identity
|
7
|
+
attr_reader :identifier, :environment_api_key, :created_date, :identity_features,
|
8
|
+
:identity_traits, :identity_uuid, :django_id
|
9
|
+
|
10
|
+
def initialize(params)
|
11
|
+
@identity_uuid = params.fetch(:identity_uuid, SecureRandom.uuid)
|
12
|
+
@created_date = params[:created_date].is_a?(String) ? Date.parse(params[:created_date]) : params[:created_date]
|
13
|
+
@identity_traits = params.fetch(:identity_traits, [])
|
14
|
+
@identity_features = params.fetch(:identity_features, Flagsmith::Engine::Identities::FeaturesList.new)
|
15
|
+
@environment_api_key = params.fetch(:environment_api_key)
|
16
|
+
@identifier = params.fetch(:identifier)
|
17
|
+
@django_id = params.fetch(:django_id, nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def composite_key
|
21
|
+
Identity.generate_composite_key(@environment_api_key, @identifier)
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_traits(traits)
|
25
|
+
existing_traits = {}
|
26
|
+
@identity_traits.each { |trait| existing_traits[trait.key] = trait }
|
27
|
+
|
28
|
+
traits.each do |trait|
|
29
|
+
if trait.value.nil?
|
30
|
+
existing_traits.delete(trait.key)
|
31
|
+
else
|
32
|
+
existing_traits[trait.key] = trait
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
@identity_traits = existing_traits.values
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
def generate_composite_key(env_key, identifier)
|
41
|
+
"#{env_key}_#{identifier}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def build(json)
|
45
|
+
identity_features = Flagsmith::Engine::Identities::FeaturesList.build(json[:identity_features])
|
46
|
+
identity_traits = json.fetch(:identity_traits, [])
|
47
|
+
.map { |t| Flagsmith::Engine::Identities::Trait.build(t) }
|
48
|
+
|
49
|
+
Identity.new(
|
50
|
+
**json.slice(:identifier, :identity_uuid, :environment_api_key, :created_date, :django_id)
|
51
|
+
.merge(identity_features: identity_features, identity_traits: identity_traits)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module Identities
|
58
|
+
# TraitModel
|
59
|
+
class Trait
|
60
|
+
attr_reader :trait_value, :trait_key
|
61
|
+
|
62
|
+
def initialize(trait_key:, trait_value:)
|
63
|
+
@trait_key = trait_key
|
64
|
+
@trait_value = trait_value
|
65
|
+
end
|
66
|
+
|
67
|
+
alias key trait_key
|
68
|
+
alias value trait_value
|
69
|
+
|
70
|
+
class << self
|
71
|
+
def build(json)
|
72
|
+
new(**json.slice(:trait_key, :trait_value))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class NotUniqueFeatureState < StandardError; end
|
78
|
+
|
79
|
+
# IdentityFeaturesList
|
80
|
+
class FeaturesList
|
81
|
+
include Enumerable
|
82
|
+
|
83
|
+
def initialize(list = [])
|
84
|
+
@list = []
|
85
|
+
list.each { |item| @list << item }
|
86
|
+
end
|
87
|
+
|
88
|
+
def <<(item)
|
89
|
+
@list.each do |v|
|
90
|
+
next unless v.django_id == item.django_id
|
91
|
+
|
92
|
+
raise NotUniqueFeatureState, "Feature state for this feature already exists, django_id: #{django_id}"
|
93
|
+
end
|
94
|
+
@list << item
|
95
|
+
end
|
96
|
+
|
97
|
+
alias push <<
|
98
|
+
|
99
|
+
def each(&block)
|
100
|
+
@list.each { |item| block&.call(item) || item }
|
101
|
+
end
|
102
|
+
|
103
|
+
class << self
|
104
|
+
def build(identity_features)
|
105
|
+
return new unless identity_features&.any?
|
106
|
+
|
107
|
+
new(
|
108
|
+
identity_features.map { |f| Flagsmith::Engine::FeatureState.build(f) }
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flagsmith
|
4
|
+
module Engine
|
5
|
+
# OrganisationModel
|
6
|
+
class Organisation
|
7
|
+
attr_reader :id, :name, :feature_analitycs, :stop_serving_flags, :persist_trait_data
|
8
|
+
|
9
|
+
def initialize(id:, name:, stop_serving_flags:, persist_trait_data:, feature_analitycs: nil)
|
10
|
+
@id = id
|
11
|
+
@name = name
|
12
|
+
@feature_analitycs = feature_analitycs
|
13
|
+
@stop_serving_flags = stop_serving_flags
|
14
|
+
@persist_trait_data = persist_trait_data
|
15
|
+
end
|
16
|
+
|
17
|
+
def unique_slug
|
18
|
+
"#{id}-#{name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def build(json)
|
23
|
+
new(**json.slice(:id, :name, :feature_analitycs, :stop_serving_flags, :persist_trait_data))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flagsmith
|
4
|
+
module Engine
|
5
|
+
# ProjectModel
|
6
|
+
class Project
|
7
|
+
attr_reader :id, :name, :organisation
|
8
|
+
attr_accessor :segments, :hide_disabled_flags
|
9
|
+
|
10
|
+
def initialize(id:, name:, organisation:, hide_disabled_flags:, segments: [])
|
11
|
+
@id = id
|
12
|
+
@name = name
|
13
|
+
@hide_disabled_flags = hide_disabled_flags
|
14
|
+
@organisation = organisation
|
15
|
+
@segments = segments
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def build(json)
|
20
|
+
segments = json.fetch(:segments, []).map { |s| Flagsmith::Engine::Segment.build(s) }
|
21
|
+
|
22
|
+
new(
|
23
|
+
**json.slice(:id, :name, :hide_disabled_flags)
|
24
|
+
.merge(organisation: Flagsmith::Engine::Organisation.build(json[:organisation]))
|
25
|
+
.merge(segments: segments)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flagsmith
|
4
|
+
module Engine
|
5
|
+
module Segments
|
6
|
+
module Constants
|
7
|
+
# Segment Rules
|
8
|
+
ALL_RULE = 'ALL'
|
9
|
+
ANY_RULE = 'ANY'
|
10
|
+
NONE_RULE = 'NONE'
|
11
|
+
|
12
|
+
RULE_TYPES = [ALL_RULE, ANY_RULE, NONE_RULE].freeze
|
13
|
+
|
14
|
+
# Segment Condition Operators
|
15
|
+
EQUAL = 'EQUAL'
|
16
|
+
GREATER_THAN = 'GREATER_THAN'
|
17
|
+
LESS_THAN = 'LESS_THAN'
|
18
|
+
LESS_THAN_INCLUSIVE = 'LESS_THAN_INCLUSIVE'
|
19
|
+
CONTAINS = 'CONTAINS'
|
20
|
+
GREATER_THAN_INCLUSIVE = 'GREATER_THAN_INCLUSIVE'
|
21
|
+
NOT_CONTAINS = 'NOT_CONTAINS'
|
22
|
+
NOT_EQUAL = 'NOT_EQUAL'
|
23
|
+
REGEX = 'REGEX'
|
24
|
+
PERCENTAGE_SPLIT = 'PERCENTAGE_SPLIT'
|
25
|
+
|
26
|
+
CONDITION_OPERATORS = [
|
27
|
+
EQUAL,
|
28
|
+
GREATER_THAN,
|
29
|
+
LESS_THAN,
|
30
|
+
LESS_THAN_INCLUSIVE,
|
31
|
+
CONTAINS,
|
32
|
+
GREATER_THAN_INCLUSIVE,
|
33
|
+
NOT_CONTAINS,
|
34
|
+
NOT_EQUAL,
|
35
|
+
REGEX,
|
36
|
+
PERCENTAGE_SPLIT
|
37
|
+
].freeze
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'constants'
|
4
|
+
require_relative '../utils/hash_func'
|
5
|
+
|
6
|
+
module Flagsmith
|
7
|
+
module Engine
|
8
|
+
module Segments
|
9
|
+
# Evaluator methods
|
10
|
+
module Evaluator
|
11
|
+
include Flagsmith::Engine::Segments::Constants
|
12
|
+
include Flagsmith::Engine::Utils::HashFunc
|
13
|
+
|
14
|
+
def get_identity_segments(environment, identity, override_traits = nil)
|
15
|
+
environment.project.segments.select do |s|
|
16
|
+
evaluate_identity_in_segment(identity, s, override_traits)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Evaluates whether a given identity is in the provided segment.
|
21
|
+
#
|
22
|
+
# :param identity: identity model object to evaluate
|
23
|
+
# :param segment: segment model object to evaluate
|
24
|
+
# :param override_traits: pass in a list of traits to use instead of those on the
|
25
|
+
# identity model itself
|
26
|
+
# :return: True if the identity is in the segment, False otherwise
|
27
|
+
def evaluate_identity_in_segment(identity, segment, override_traits = nil)
|
28
|
+
segment.rules&.length&.positive? &&
|
29
|
+
segment.rules.all? do |rule|
|
30
|
+
traits_match_segment_rule(
|
31
|
+
override_traits || identity.identity_traits,
|
32
|
+
rule,
|
33
|
+
segment.id,
|
34
|
+
identity.django_id || identity.composite_key
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def traits_match_segment_rule(identity_traits, rule, segment_id, identity_id)
|
40
|
+
matching_block = lambda { |condition|
|
41
|
+
traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
|
42
|
+
}
|
43
|
+
|
44
|
+
matches_conditions =
|
45
|
+
if rule.conditions&.length&.positive?
|
46
|
+
rule.conditions.send(rule.matching_function, &matching_block)
|
47
|
+
else true
|
48
|
+
end
|
49
|
+
|
50
|
+
matches_conditions &&
|
51
|
+
rule.rules.all? { |r| traits_match_segment_rule(identity_traits, r, segment_id, identity_id) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
|
55
|
+
if condition.operator == PERCENTAGE_SPLIT
|
56
|
+
return hashed_percentage_for_object_ids([segment_id, identity_id]) <= condition.value.to_f
|
57
|
+
end
|
58
|
+
|
59
|
+
trait = identity_traits.find { |t| t.key == condition.property }
|
60
|
+
|
61
|
+
return condition.match_trait_value?(trait.value) if trait
|
62
|
+
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'constants'
|
4
|
+
|
5
|
+
module Flagsmith
|
6
|
+
module Engine
|
7
|
+
# SegmentModel
|
8
|
+
class Segment
|
9
|
+
attr_reader :id, :name
|
10
|
+
attr_accessor :rules, :feature_states
|
11
|
+
|
12
|
+
def initialize(id:, name:, rules: [], feature_states: [])
|
13
|
+
@id = id
|
14
|
+
@name = name
|
15
|
+
@rules = rules
|
16
|
+
@feature_states = feature_states
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def build(json)
|
21
|
+
feature_states = json.fetch(:feature_states, []).map { |fs| Flagsmith::Engine::FeatureState.build(fs) }
|
22
|
+
rules = json.fetch(:rules, []).map { |rule| Flagsmith::Engine::Segments::Rule.build(rule) }
|
23
|
+
|
24
|
+
new(
|
25
|
+
**json.slice(:id, :name)
|
26
|
+
.merge(feature_states: feature_states, rules: rules)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Segments
|
33
|
+
# SegmentConditionModel
|
34
|
+
class Condition
|
35
|
+
include Constants
|
36
|
+
attr_reader :operator, :value, :property
|
37
|
+
|
38
|
+
MATCHING_FUNCTIONS = {
|
39
|
+
EQUAL => ->(other_value, self_value) { other_value == self_value },
|
40
|
+
GREATER_THAN => ->(other_value, self_value) { other_value > self_value },
|
41
|
+
GREATER_THAN_INCLUSIVE => ->(other_value, self_value) { other_value >= self_value },
|
42
|
+
LESS_THAN => ->(other_value, self_value) { other_value < self_value },
|
43
|
+
LESS_THAN_INCLUSIVE => ->(other_value, self_value) { other_value <= self_value },
|
44
|
+
NOT_EQUAL => ->(other_value, self_value) { other_value != self_value },
|
45
|
+
CONTAINS => ->(other_value, self_value) { other_value.include? self_value },
|
46
|
+
|
47
|
+
NOT_CONTAINS => ->(other_value, self_value) { !other_value.include? self_value },
|
48
|
+
REGEX => ->(other_value, self_value) { other_value.match? self_value }
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
def initialize(operator:, value:, property: nil)
|
52
|
+
@operator = operator
|
53
|
+
@value = value
|
54
|
+
@property = property
|
55
|
+
end
|
56
|
+
|
57
|
+
def match_trait_value?(trait_value)
|
58
|
+
if @value.is_a?(String) && @value.match?(/:semver$/)
|
59
|
+
trait_value = Semantic::Version.new(trait_value.gsub(/:semver$/, ''))
|
60
|
+
end
|
61
|
+
|
62
|
+
type_as_trait_value = format_to_type_of(trait_value)
|
63
|
+
formatted_value = type_as_trait_value ? type_as_trait_value.call(@value) : @value
|
64
|
+
|
65
|
+
MATCHING_FUNCTIONS[operator]&.call(trait_value, formatted_value)
|
66
|
+
end
|
67
|
+
|
68
|
+
# rubocop:disable Metrics/AbcSize
|
69
|
+
def format_to_type_of(input)
|
70
|
+
{
|
71
|
+
'String' => ->(v) { v.to_s },
|
72
|
+
'Semantic::Version' => ->(v) { Semantic::Version.new(v.to_s.gsub(/:semver$/, '')) },
|
73
|
+
'TrueClass' => ->(v) { ['True', 'true', 'TRUE', true, 1, '1'].include?(v) ? true : false },
|
74
|
+
'FalseClass' => ->(v) { ['False', 'false', 'FALSE', false, 0, '0'].include?(v) ? false : true },
|
75
|
+
'Integer' => ->(v) { v.to_i },
|
76
|
+
'Float' => ->(v) { v.to_f }
|
77
|
+
}[input.class.to_s]
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/AbcSize
|
80
|
+
|
81
|
+
class << self
|
82
|
+
def build(json)
|
83
|
+
new(**json.slice(:operator, :value).merge(property: json[:property_]))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# SegmentRuleModel
|
89
|
+
class Rule
|
90
|
+
include Constants
|
91
|
+
MATCHING_FUNCTIONS = {
|
92
|
+
ANY_RULE => :any?,
|
93
|
+
ALL_RULE => :all?,
|
94
|
+
NONE_RULE => :none?
|
95
|
+
}.freeze
|
96
|
+
|
97
|
+
attr_accessor :type, :rules, :conditions
|
98
|
+
|
99
|
+
def initialize(type: nil, rules: [], conditions: [])
|
100
|
+
@type = type
|
101
|
+
@rules = rules
|
102
|
+
@conditions = conditions
|
103
|
+
end
|
104
|
+
|
105
|
+
def matching_function
|
106
|
+
MATCHING_FUNCTIONS[type]
|
107
|
+
end
|
108
|
+
|
109
|
+
class << self
|
110
|
+
def build(json)
|
111
|
+
rules = json.fetch(:rules, []).map { |r| Flagsmith::Engine::Segments::Rule.build(r) }
|
112
|
+
conditions = json.fetch(:conditions, []).map { |c| Flagsmith::Engine::Segments::Condition.build(c) }
|
113
|
+
new(
|
114
|
+
type: json[:type], rules: rules, conditions: conditions
|
115
|
+
)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module Flagsmith
|
6
|
+
module Engine
|
7
|
+
module Utils
|
8
|
+
# HashFunction
|
9
|
+
module HashFunc
|
10
|
+
# Given a list of object ids, get a floating point number between 0 (inclusive) and
|
11
|
+
# 100 (exclusive) based on the hash of those ids. This should give the same value
|
12
|
+
# every time for any list of ids.
|
13
|
+
#
|
14
|
+
# :param object_ids: list of object ids to calculate the hash for
|
15
|
+
# :param iterations: num times to include each id in the generated string to hash
|
16
|
+
# :return: (float) number between 0 (inclusive) and 100 (exclusive)
|
17
|
+
def hashed_percentage_for_object_ids(object_ids, iterations = 1)
|
18
|
+
to_hash = (object_ids.map(&:to_s) * iterations).flatten.join(',')
|
19
|
+
|
20
|
+
hashed_value = Digest::MD5.hexdigest(to_hash.encode('utf-8'))
|
21
|
+
hashed_value_as_int = hashed_value.to_i(16)
|
22
|
+
value = ((hashed_value_as_int % 9999).to_f / 9998) * 100
|
23
|
+
|
24
|
+
# since we want a number between 0 (inclusive) and 100 (exclusive), in the
|
25
|
+
# unlikely case that we get the exact number 100, we call the method again
|
26
|
+
# and increase the number of iterations to ensure we get a different result
|
27
|
+
return hashed_percentage_for_object_ids(object_ids, iterations + 1) if value == 100
|
28
|
+
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flagsmith
|
4
|
+
# Hash#slice was added in ruby version 2.5
|
5
|
+
module HashSlice
|
6
|
+
def slice(*keys)
|
7
|
+
select { |key, _value| keys.include?(key) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Hash.include Flagsmith::HashSlice if RUBY_VERSION < '2.5.0'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flagsmith
|
4
|
+
# Used to control how often we send data(in seconds)
|
5
|
+
class AnalyticsProcessor
|
6
|
+
ENDPOINT = 'analytics/flags/'
|
7
|
+
TIMER = 10
|
8
|
+
attr_reader :last_flushed, :timeout, :analytics_data
|
9
|
+
|
10
|
+
# AnalyticsProcessor is used to track how often individual Flags are evaluated within
|
11
|
+
# the Flagsmith SDK. Docs: https://docs.flagsmith.com/advanced-use/flag-analytics.
|
12
|
+
#
|
13
|
+
# data[:environment_key] environment key obtained from the Flagsmith UI
|
14
|
+
# data[:base_api_url] base api url to override when using self hosted version
|
15
|
+
# data[:timeout] used to tell requests to stop waiting for a response after a
|
16
|
+
# given number of seconds
|
17
|
+
def initialize(data)
|
18
|
+
@last_flushed = Time.now
|
19
|
+
@analytics_data = {}
|
20
|
+
@api_client = data.fetch(:api_client)
|
21
|
+
@timeout = data.fetch(:timeout, 3)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Sends all the collected data to the api asynchronously and resets the timer
|
25
|
+
def flush
|
26
|
+
return if @analytics_data.empty?
|
27
|
+
|
28
|
+
@api_client.post(ENDPOINT, @analytics_data.to_json)
|
29
|
+
|
30
|
+
@analytics_data = {}
|
31
|
+
@last_flushed = Time.now
|
32
|
+
end
|
33
|
+
|
34
|
+
def track_feature(feature_id)
|
35
|
+
@analytics_data[feature_id] = @analytics_data.fetch(feature_id, 0) + 1
|
36
|
+
flush if (Time.now - @last_flushed) > TIMER * 1000
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|