Skip to content

Commit b83e35c

Browse files
Merge pull request #19 from Flagsmith/release/3.1.0
Release 3.1.0
2 parents 0ea82ad + 8784de6 commit b83e35c

19 files changed

+261
-154
lines changed

.github/workflows/pull_request.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ on:
1414
- synchronize
1515
- reopened
1616
- ready_for_review
17+
branches:
18+
- main
19+
- release/**
1720

1821
push:
1922
branches:

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ build-iPhoneSimulator/
5656

5757
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
5858
.rvmrc
59+
60+
# direnv / asdf set up
61+
.direnv
62+
.tool-versions

Gemfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
flagsmith (3.0.2)
4+
flagsmith (3.1.0)
55
faraday
66
faraday-retry
77
faraday_middleware

lib/flagsmith.rb

+149-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
require 'flagsmith/sdk/intervals'
1616
require 'flagsmith/sdk/pooling_manager'
1717
require 'flagsmith/sdk/models/flags'
18-
require 'flagsmith/sdk/instance_methods'
18+
require 'flagsmith/sdk/models/segments'
1919

2020
require 'flagsmith/engine/core'
2121

@@ -24,8 +24,6 @@ module Flagsmith
2424
# Ruby client for flagsmith.com
2525
class Client
2626
extend Forwardable
27-
include Flagsmith::SDK::InstanceMethods
28-
include Flagsmith::Engine::Core
2927
# A Flagsmith client.
3028
#
3129
# Provides an interface for interacting with the Flagsmith http API.
@@ -37,9 +35,11 @@ class Client
3735
# feature_enabled = environment_flags.is_feature_enabled('foo')
3836
# feature_value = identity_flags.get_feature_value('foo')
3937
#
40-
# identity_flags = flagsmith.get_identity_flags('identifier', 'foo': 'bar')
38+
# identity_flags = flagsmith.get_identity_flags('identifier', {'foo': 'bar'})
4139
# feature_enabled_for_identity = identity_flags.is_feature_enabled('foo')
4240
# feature_value_for_identity = identity_flags.get_feature_value('foo')
41+
#
42+
# identity_segments = flagsmith.get_identity_segments('identifier', {'foo': 'bar'})
4343

4444
# Available Configs.
4545
#
@@ -58,12 +58,17 @@ def initialize(config)
5858
api_client
5959
analytics_processor
6060
environment_data_polling_manager
61+
engine
6162
end
6263

6364
def api_client
6465
@api_client ||= Flagsmith::ApiClient.new(@config)
6566
end
6667

68+
def engine
69+
@engine ||= Flagsmith::Engine::Engine.new
70+
end
71+
6772
def analytics_processor
6873
return nil unless @config.enable_analytics?
6974

@@ -94,5 +99,145 @@ def environment_from_api
9499
environment_data = api_client.get(@config.environment_url).body
95100
Flagsmith::Engine::Environment.build(environment_data)
96101
end
102+
103+
# Get all the default for flags for the current environment.
104+
# @returns Flags object holding all the flags for the current environment.
105+
def get_environment_flags # rubocop:disable Naming/AccessorMethodName
106+
return environment_flags_from_document if @config.local_evaluation?
107+
108+
environment_flags_from_api
109+
end
110+
111+
# Get all the flags for the current environment for a given identity. Will also
112+
# upsert all traits to the Flagsmith API for future evaluations. Providing a
113+
# trait with a value of None will remove the trait from the identity if it exists.
114+
#
115+
# identifier a unique identifier for the identity in the current
116+
# environment, e.g. email address, username, uuid
117+
# traits { key => value } is a dictionary of traits to add / update on the identity in
118+
# Flagsmith, e.g. { "num_orders": 10 }
119+
# returns Flags object holding all the flags for the given identity.
120+
def get_identity_flags(identifier, **traits)
121+
return get_identity_flags_from_document(identifier, traits) if environment
122+
123+
get_identity_flags_from_api(identifier, traits)
124+
end
125+
126+
def feature_enabled?(feature_name, default: false)
127+
flag = get_environment_flags[feature_name]
128+
return default if flag.nil?
129+
130+
flag.enabled?
131+
end
132+
133+
def feature_enabled_for_identity?(feature_name, user_id, default: false)
134+
flag = get_identity_flags(user_id)[feature_name]
135+
return default if flag.nil?
136+
137+
flag.enabled?
138+
end
139+
140+
def get_value(feature_name, default: nil)
141+
flag = get_environment_flags[feature_name]
142+
return default if flag.nil?
143+
144+
flag.value
145+
end
146+
147+
def get_value_for_identity(feature_name, user_id = nil, default: nil)
148+
flag = get_identity_flags(user_id)[feature_name]
149+
return default if flag.nil?
150+
151+
flag.value
152+
end
153+
154+
def get_identity_segments(identifier, traits = {})
155+
unless environment
156+
raise Flagsmith::ClientError,
157+
'Local evaluation required to obtain identity segments.'
158+
end
159+
160+
identity_model = build_identity_model(identifier, traits)
161+
segment_models = engine.get_identity_segments(environment, identity_model)
162+
return segment_models.map { |sm| Flagsmith::Segments::Segment.new(id: sm.id, name: sm.name) }.compact
163+
end
164+
165+
private
166+
167+
def environment_flags_from_document
168+
Flagsmith::Flags::Collection.from_feature_state_models(
169+
engine.get_environment_feature_states(environment),
170+
analytics_processor: analytics_processor,
171+
default_flag_handler: default_flag_handler
172+
)
173+
end
174+
175+
def get_identity_flags_from_document(identifier, traits = {})
176+
identity_model = build_identity_model(identifier, traits)
177+
178+
Flagsmith::Flags::Collection.from_feature_state_models(
179+
engine.get_identity_feature_states(environment, identity_model),
180+
analytics_processor: analytics_processor,
181+
default_flag_handler: default_flag_handler
182+
)
183+
end
184+
185+
def environment_flags_from_api
186+
rescue_with_default_handler do
187+
api_flags = api_client.get(@config.environment_flags_url).body
188+
api_flags = api_flags.select { |flag| flag[:feature_segment].nil? }
189+
Flagsmith::Flags::Collection.from_api(
190+
api_flags,
191+
analytics_processor: analytics_processor,
192+
default_flag_handler: default_flag_handler
193+
)
194+
end
195+
end
196+
197+
def get_identity_flags_from_api(identifier, traits = {})
198+
rescue_with_default_handler do
199+
data = generate_identities_data(identifier, traits)
200+
json_response = api_client.post(@config.identities_url, data.to_json).body
201+
202+
Flagsmith::Flags::Collection.from_api(
203+
json_response[:flags],
204+
analytics_processor: analytics_processor,
205+
default_flag_handler: default_flag_handler
206+
)
207+
end
208+
end
209+
210+
def rescue_with_default_handler
211+
yield
212+
rescue StandardError
213+
if default_flag_handler
214+
return Flagsmith::Flags::Collection.new(
215+
{},
216+
default_flag_handler: default_flag_handler
217+
)
218+
end
219+
raise
220+
end
221+
222+
def build_identity_model(identifier, traits = {})
223+
unless environment
224+
raise Flagsmith::ClientError,
225+
'Unable to build identity model when no local environment present.'
226+
end
227+
228+
trait_models = traits.map do |key, value|
229+
Flagsmith::Engine::Identities::Trait.new(trait_key: key, trait_value: value)
230+
end
231+
Flagsmith::Engine::Identity.new(
232+
identity_traits: trait_models, environment_api_key: environment_key, identifier: identifier
233+
)
234+
end
235+
236+
def generate_identities_data(identifier, traits = {})
237+
{
238+
identifier: identifier,
239+
traits: traits.map { |key, value| { trait_key: key, trait_value: value } }
240+
}
241+
end
97242
end
98243
end

lib/flagsmith/engine/core.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
module Flagsmith
1616
module Engine
1717
# Flags engine methods
18-
module Core
18+
class Engine
1919
include Flagsmith::Engine::Segments::Evaluator
2020

2121
def get_identity_feature_state(environment, identity, feature_name, override_traits = nil)

lib/flagsmith/engine/segments/constants.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ module Constants
2222
NOT_EQUAL = 'NOT_EQUAL'
2323
REGEX = 'REGEX'
2424
PERCENTAGE_SPLIT = 'PERCENTAGE_SPLIT'
25+
IS_SET = 'IS_SET'
26+
IS_NOT_SET = 'IS_NOT_SET'
27+
MODULO = 'MODULO'
2528

2629
CONDITION_OPERATORS = [
2730
EQUAL,
@@ -33,7 +36,8 @@ module Constants
3336
NOT_CONTAINS,
3437
NOT_EQUAL,
3538
REGEX,
36-
PERCENTAGE_SPLIT
39+
PERCENTAGE_SPLIT,
40+
MODULO
3741
].freeze
3842
end
3943
end

lib/flagsmith/engine/segments/evaluator.rb

+14-2
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,24 @@ def traits_match_segment_condition(identity_traits, condition, segment_id, ident
5656
return hashed_percentage_for_object_ids([segment_id, identity_id]) <= condition.value.to_f
5757
end
5858

59-
trait = identity_traits.find { |t| t.key == condition.property }
59+
trait = identity_traits.find { |t| t.key.to_s == condition.property }
6060

61-
return condition.match_trait_value?(trait.value) if trait
61+
if [IS_SET, IS_NOT_SET].include?(condition.operator)
62+
return handle_trait_existence_conditions(trait, condition.operator)
63+
end
64+
65+
return condition.match_trait_value?(trait.trait_value) if trait
6266

6367
false
6468
end
69+
70+
private
71+
72+
def handle_trait_existence_conditions(matching_trait, operator)
73+
return operator == IS_NOT_SET if matching_trait.nil?
74+
75+
operator == IS_SET
76+
end
6577
end
6678
end
6779
end

lib/flagsmith/engine/segments/models.rb

+10
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ def initialize(operator:, value:, property: nil)
5555
end
5656

5757
def match_trait_value?(trait_value)
58+
# handle some exceptions
5859
if @value.is_a?(String) && @value.match?(/:semver$/)
5960
trait_value = Semantic::Version.new(trait_value.gsub(/:semver$/, ''))
6061
end
6162

63+
return match_modulo_value(trait_value) if @operator == MODULO
64+
6265
type_as_trait_value = format_to_type_of(trait_value)
6366
formatted_value = type_as_trait_value ? type_as_trait_value.call(@value) : @value
6467

@@ -78,6 +81,13 @@ def format_to_type_of(input)
7881
end
7982
# rubocop:enable Metrics/AbcSize
8083

84+
def match_modulo_value(trait_value)
85+
divisor, remainder = @value.split('|')
86+
trait_value.is_a?(Numeric) && trait_value % divisor.to_f == remainder.to_f
87+
rescue StandardError
88+
false
89+
end
90+
8191
class << self
8292
def build(json)
8393
new(**json.slice(:operator, :value).merge(property: json[:property_]))

0 commit comments

Comments
 (0)