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