Skip to content

Commit

Permalink
Merge pull request #19 from Flagsmith/release/3.1.0
Browse files Browse the repository at this point in the history
Release 3.1.0
  • Loading branch information
matthewelwell authored Nov 1, 2022
2 parents 0ea82ad + 8784de6 commit b83e35c
Show file tree
Hide file tree
Showing 19 changed files with 261 additions and 154 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ on:
- synchronize
- reopened
- ready_for_review
branches:
- main
- release/**

push:
branches:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ build-iPhoneSimulator/

# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc

# direnv / asdf set up
.direnv
.tool-versions
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
flagsmith (3.0.2)
flagsmith (3.1.0)
faraday
faraday-retry
faraday_middleware
Expand Down
153 changes: 149 additions & 4 deletions lib/flagsmith.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
require 'flagsmith/sdk/intervals'
require 'flagsmith/sdk/pooling_manager'
require 'flagsmith/sdk/models/flags'
require 'flagsmith/sdk/instance_methods'
require 'flagsmith/sdk/models/segments'

require 'flagsmith/engine/core'

Expand All @@ -24,8 +24,6 @@ module Flagsmith
# Ruby client for flagsmith.com
class Client
extend Forwardable
include Flagsmith::SDK::InstanceMethods
include Flagsmith::Engine::Core
# A Flagsmith client.
#
# Provides an interface for interacting with the Flagsmith http API.
Expand All @@ -37,9 +35,11 @@ class Client
# feature_enabled = environment_flags.is_feature_enabled('foo')
# feature_value = identity_flags.get_feature_value('foo')
#
# identity_flags = flagsmith.get_identity_flags('identifier', 'foo': 'bar')
# identity_flags = flagsmith.get_identity_flags('identifier', {'foo': 'bar'})
# feature_enabled_for_identity = identity_flags.is_feature_enabled('foo')
# feature_value_for_identity = identity_flags.get_feature_value('foo')
#
# identity_segments = flagsmith.get_identity_segments('identifier', {'foo': 'bar'})

# Available Configs.
#
Expand All @@ -58,12 +58,17 @@ def initialize(config)
api_client
analytics_processor
environment_data_polling_manager
engine
end

def api_client
@api_client ||= Flagsmith::ApiClient.new(@config)
end

def engine
@engine ||= Flagsmith::Engine::Engine.new
end

def analytics_processor
return nil unless @config.enable_analytics?

Expand Down Expand Up @@ -94,5 +99,145 @@ def environment_from_api
environment_data = api_client.get(@config.environment_url).body
Flagsmith::Engine::Environment.build(environment_data)
end

# Get all the default for flags for the current environment.
# @returns Flags object holding all the flags for the current environment.
def get_environment_flags # rubocop:disable Naming/AccessorMethodName
return environment_flags_from_document if @config.local_evaluation?

environment_flags_from_api
end

# Get all the flags for the current environment for a given identity. Will also
# upsert all traits to the Flagsmith API for future evaluations. Providing a
# trait with a value of None will remove the trait from the identity if it exists.
#
# identifier a unique identifier for the identity in the current
# environment, e.g. email address, username, uuid
# traits { key => value } is a dictionary of traits to add / update on the identity in
# Flagsmith, e.g. { "num_orders": 10 }
# returns Flags object holding all the flags for the given identity.
def get_identity_flags(identifier, **traits)
return get_identity_flags_from_document(identifier, traits) if environment

get_identity_flags_from_api(identifier, traits)
end

def feature_enabled?(feature_name, default: false)
flag = get_environment_flags[feature_name]
return default if flag.nil?

flag.enabled?
end

def feature_enabled_for_identity?(feature_name, user_id, default: false)
flag = get_identity_flags(user_id)[feature_name]
return default if flag.nil?

flag.enabled?
end

def get_value(feature_name, default: nil)
flag = get_environment_flags[feature_name]
return default if flag.nil?

flag.value
end

def get_value_for_identity(feature_name, user_id = nil, default: nil)
flag = get_identity_flags(user_id)[feature_name]
return default if flag.nil?

flag.value
end

def get_identity_segments(identifier, traits = {})
unless environment
raise Flagsmith::ClientError,
'Local evaluation required to obtain identity segments.'
end

identity_model = build_identity_model(identifier, traits)
segment_models = engine.get_identity_segments(environment, identity_model)
return segment_models.map { |sm| Flagsmith::Segments::Segment.new(id: sm.id, name: sm.name) }.compact
end

private

def environment_flags_from_document
Flagsmith::Flags::Collection.from_feature_state_models(
engine.get_environment_feature_states(environment),
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler
)
end

def get_identity_flags_from_document(identifier, traits = {})
identity_model = build_identity_model(identifier, traits)

Flagsmith::Flags::Collection.from_feature_state_models(
engine.get_identity_feature_states(environment, identity_model),
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler
)
end

def environment_flags_from_api
rescue_with_default_handler do
api_flags = api_client.get(@config.environment_flags_url).body
api_flags = api_flags.select { |flag| flag[:feature_segment].nil? }
Flagsmith::Flags::Collection.from_api(
api_flags,
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler
)
end
end

def get_identity_flags_from_api(identifier, traits = {})
rescue_with_default_handler do
data = generate_identities_data(identifier, traits)
json_response = api_client.post(@config.identities_url, data.to_json).body

Flagsmith::Flags::Collection.from_api(
json_response[:flags],
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler
)
end
end

def rescue_with_default_handler
yield
rescue StandardError
if default_flag_handler
return Flagsmith::Flags::Collection.new(
{},
default_flag_handler: default_flag_handler
)
end
raise
end

def build_identity_model(identifier, traits = {})
unless environment
raise Flagsmith::ClientError,
'Unable to build identity model when no local environment present.'
end

trait_models = traits.map do |key, value|
Flagsmith::Engine::Identities::Trait.new(trait_key: key, trait_value: value)
end
Flagsmith::Engine::Identity.new(
identity_traits: trait_models, environment_api_key: environment_key, identifier: identifier
)
end

def generate_identities_data(identifier, traits = {})
{
identifier: identifier,
traits: traits.map { |key, value| { trait_key: key, trait_value: value } }
}
end
end
end
2 changes: 1 addition & 1 deletion lib/flagsmith/engine/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
module Flagsmith
module Engine
# Flags engine methods
module Core
class Engine
include Flagsmith::Engine::Segments::Evaluator

def get_identity_feature_state(environment, identity, feature_name, override_traits = nil)
Expand Down
6 changes: 5 additions & 1 deletion lib/flagsmith/engine/segments/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ module Constants
NOT_EQUAL = 'NOT_EQUAL'
REGEX = 'REGEX'
PERCENTAGE_SPLIT = 'PERCENTAGE_SPLIT'
IS_SET = 'IS_SET'
IS_NOT_SET = 'IS_NOT_SET'
MODULO = 'MODULO'

CONDITION_OPERATORS = [
EQUAL,
Expand All @@ -33,7 +36,8 @@ module Constants
NOT_CONTAINS,
NOT_EQUAL,
REGEX,
PERCENTAGE_SPLIT
PERCENTAGE_SPLIT,
MODULO
].freeze
end
end
Expand Down
16 changes: 14 additions & 2 deletions lib/flagsmith/engine/segments/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,24 @@ def traits_match_segment_condition(identity_traits, condition, segment_id, ident
return hashed_percentage_for_object_ids([segment_id, identity_id]) <= condition.value.to_f
end

trait = identity_traits.find { |t| t.key == condition.property }
trait = identity_traits.find { |t| t.key.to_s == condition.property }

return condition.match_trait_value?(trait.value) if trait
if [IS_SET, IS_NOT_SET].include?(condition.operator)
return handle_trait_existence_conditions(trait, condition.operator)
end

return condition.match_trait_value?(trait.trait_value) if trait

false
end

private

def handle_trait_existence_conditions(matching_trait, operator)
return operator == IS_NOT_SET if matching_trait.nil?

operator == IS_SET
end
end
end
end
Expand Down
10 changes: 10 additions & 0 deletions lib/flagsmith/engine/segments/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ def initialize(operator:, value:, property: nil)
end

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

return match_modulo_value(trait_value) if @operator == MODULO

type_as_trait_value = format_to_type_of(trait_value)
formatted_value = type_as_trait_value ? type_as_trait_value.call(@value) : @value

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

def match_modulo_value(trait_value)
divisor, remainder = @value.split('|')
trait_value.is_a?(Numeric) && trait_value % divisor.to_f == remainder.to_f
rescue StandardError
false
end

class << self
def build(json)
new(**json.slice(:operator, :value).merge(property: json[:property_]))
Expand Down
Loading

0 comments on commit b83e35c

Please sign in to comment.