flagsmith 3.1.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 540a1b7662a35e22fd9acd04ff632ff0ac3178653aa1b154406d0b5f27420f70
4
- data.tar.gz: 47a63fc9e165518bc8e7b0f946e3ebea50676771e4165ac9e383104950a54ce6
3
+ metadata.gz: f7e45006cc3caabc6941c7daaf58f339940cb1ea1fc3ec76be0a7897d84212fa
4
+ data.tar.gz: 32703828745194ea1befb791266e592e888c7fac888fd35c4cc6b6e9b6f3e204
5
5
  SHA512:
6
- metadata.gz: 4f328cea0c2c2b56d0fda76244aab72a0314d590308f2ed2fbd458ebb2ba215c1feb57b02b1ac06034d6cebd19a2cecc236d496b8b929caf4083cb949e912b97
7
- data.tar.gz: 5325e0fd19633dd3f91665635ebb10e0a3c88d447df1a5db8bcef3c0d492f392fa477a22ac2ad04a7b918c2493c3f80e56e69634628c5df9ae16876a1ba11103
6
+ metadata.gz: fa77df821acafa8ccc9d0580845573b47fe0287c1937a7f8200d08532f559d77af1c41e6862a52a0ab90fec30cc020ea63068120aa95a95da41cff2b2a48e84d
7
+ data.tar.gz: 25f98c085b4b8253356fbc203e5605fa6ac53930e7ac8bbe7d2be1f5ba8dc8bf0374ca9a85b57bebafb08b9e57077dcc18637fca9b9d91a758ec404398de663a
data/.rubocop.yml CHANGED
@@ -4,8 +4,10 @@ Layout/HashAlignment:
4
4
  AllowMultipleStyles: true
5
5
  EnforcedColonStyle: key
6
6
  AllCops:
7
+ TargetRubyVersion: 3.0 # Pin to flagsmith.gemspec required_ruby_version
7
8
  NewCops: enable
8
9
  SuggestExtensions: false
9
10
  Exclude:
10
11
  - 'spec/**/*'
11
12
  - 'example/**/*'
13
+ - 'vendor/**/*'
data/.rubocop_todo.yml CHANGED
@@ -1,29 +1,22 @@
1
1
  # This configuration was generated by
2
- # `rubocop --auto-gen-config --auto-gen-only-exclude`
3
- # on 2020-12-30 22:56:14 UTC using RuboCop version 1.7.0.
2
+ # `rubocop --auto-gen-config`
3
+ # on 2023-07-20 10:26:52 UTC using RuboCop version 1.54.2.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 1
10
- # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
11
- # IgnoredMethods: refine
12
- Metrics/BlockLength:
9
+ # Offense count: 6
10
+ # Configuration parameters: EnforcedStyle, AllowedGems, Include.
11
+ # SupportedStyles: Gemfile, gems.rb, gemspec
12
+ # Include: **/*.gemspec, **/Gemfile, **/gems.rb
13
+ Gemspec/DevelopmentDependencies:
13
14
  Exclude:
14
- - '**/*.gemspec'
15
-
16
- # Offense count: 1
17
- # Configuration parameters: AllowedMethods.
18
- # AllowedMethods: respond_to_missing?
19
- Style/OptionalBooleanParameter:
20
- Exclude:
21
- - 'lib/flagsmith.rb'
15
+ - 'flagsmith.gemspec'
22
16
 
23
- # Offense count: 1
24
- # Cop supports --auto-correct.
25
- # Configuration parameters: AutoCorrect, Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
17
+ # Offense count: 4
18
+ # This cop supports safe autocorrection (--autocorrect).
19
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
26
20
  # URISchemes: http, https
27
21
  Layout/LineLength:
28
- Exclude:
29
- - 'flagsmith.gemspec'
22
+ Max: 183
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.4.0
1
+ 3.1.4
data/Gemfile.lock CHANGED
@@ -1,84 +1,75 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flagsmith (3.1.1)
5
- faraday
4
+ flagsmith (4.0.0)
5
+ faraday (>= 2.0.1)
6
6
  faraday-retry
7
- faraday_middleware
8
7
  semantic
9
8
 
10
9
  GEM
11
10
  remote: https://rubygems.org/
12
11
  specs:
13
- ast (2.4.1)
12
+ ast (2.4.2)
13
+ byebug (11.1.3)
14
14
  coderay (1.1.3)
15
- diff-lcs (1.4.4)
16
- faraday (1.10.0)
17
- faraday-em_http (~> 1.0)
18
- faraday-em_synchrony (~> 1.0)
19
- faraday-excon (~> 1.1)
20
- faraday-httpclient (~> 1.0)
21
- faraday-multipart (~> 1.0)
22
- faraday-net_http (~> 1.0)
23
- faraday-net_http_persistent (~> 1.0)
24
- faraday-patron (~> 1.0)
25
- faraday-rack (~> 1.0)
26
- faraday-retry (~> 1.0)
27
- ruby2_keywords (>= 0.0.4)
28
- faraday-em_http (1.0.0)
29
- faraday-em_synchrony (1.0.0)
30
- faraday-excon (1.1.0)
31
- faraday-httpclient (1.0.1)
32
- faraday-multipart (1.0.4)
33
- multipart-post (~> 2)
34
- faraday-net_http (1.0.1)
35
- faraday-net_http_persistent (1.2.0)
36
- faraday-patron (1.0.0)
37
- faraday-rack (1.0.0)
38
- faraday-retry (1.0.3)
39
- faraday_middleware (1.2.0)
40
- faraday (~> 1.0)
41
- gem-release (2.2.0)
15
+ diff-lcs (1.5.0)
16
+ faraday (2.9.0)
17
+ faraday-net_http (>= 2.0, < 3.2)
18
+ faraday-net_http (3.1.0)
19
+ net-http
20
+ faraday-retry (2.2.0)
21
+ faraday (~> 2.0)
22
+ gem-release (2.2.2)
23
+ json (2.7.1)
24
+ language_server-protocol (3.17.0.3)
42
25
  method_source (1.0.0)
43
- multipart-post (2.2.3)
44
- parallel (1.20.1)
45
- parser (3.0.0.0)
26
+ net-http (0.4.1)
27
+ uri
28
+ parallel (1.24.0)
29
+ parser (3.3.0.4)
46
30
  ast (~> 2.4.1)
47
- pry (0.14.1)
31
+ racc
32
+ pry (0.14.2)
48
33
  coderay (~> 1.1)
49
34
  method_source (~> 1.0)
50
- rainbow (3.0.0)
51
- rake (13.0.3)
52
- regexp_parser (2.0.3)
53
- rexml (3.2.5)
54
- rspec (3.10.0)
55
- rspec-core (~> 3.10.0)
56
- rspec-expectations (~> 3.10.0)
57
- rspec-mocks (~> 3.10.0)
58
- rspec-core (3.10.1)
59
- rspec-support (~> 3.10.0)
60
- rspec-expectations (3.10.1)
35
+ pry-byebug (3.10.1)
36
+ byebug (~> 11.0)
37
+ pry (>= 0.13, < 0.15)
38
+ racc (1.7.3)
39
+ rainbow (3.1.1)
40
+ rake (13.1.0)
41
+ regexp_parser (2.9.0)
42
+ rexml (3.2.6)
43
+ rspec (3.12.0)
44
+ rspec-core (~> 3.12.0)
45
+ rspec-expectations (~> 3.12.0)
46
+ rspec-mocks (~> 3.12.0)
47
+ rspec-core (3.12.2)
48
+ rspec-support (~> 3.12.0)
49
+ rspec-expectations (3.12.3)
61
50
  diff-lcs (>= 1.2.0, < 2.0)
62
- rspec-support (~> 3.10.0)
63
- rspec-mocks (3.10.1)
51
+ rspec-support (~> 3.12.0)
52
+ rspec-mocks (3.12.6)
64
53
  diff-lcs (>= 1.2.0, < 2.0)
65
- rspec-support (~> 3.10.0)
66
- rspec-support (3.10.1)
67
- rubocop (1.7.0)
54
+ rspec-support (~> 3.12.0)
55
+ rspec-support (3.12.1)
56
+ rubocop (1.60.0)
57
+ json (~> 2.3)
58
+ language_server-protocol (>= 3.17.0)
68
59
  parallel (~> 1.10)
69
- parser (>= 2.7.1.5)
60
+ parser (>= 3.3.0.2)
70
61
  rainbow (>= 2.2.2, < 4.0)
71
62
  regexp_parser (>= 1.8, < 3.0)
72
- rexml
73
- rubocop-ast (>= 1.2.0, < 2.0)
63
+ rexml (>= 3.2.5, < 4.0)
64
+ rubocop-ast (>= 1.30.0, < 2.0)
74
65
  ruby-progressbar (~> 1.7)
75
- unicode-display_width (>= 1.4.0, < 2.0)
76
- rubocop-ast (1.3.0)
77
- parser (>= 2.7.1.5)
78
- ruby-progressbar (1.10.1)
79
- ruby2_keywords (0.0.5)
66
+ unicode-display_width (>= 2.4.0, < 3.0)
67
+ rubocop-ast (1.30.0)
68
+ parser (>= 3.2.1.0)
69
+ ruby-progressbar (1.13.0)
80
70
  semantic (1.6.1)
81
- unicode-display_width (1.7.0)
71
+ unicode-display_width (2.5.0)
72
+ uri (0.13.0)
82
73
 
83
74
  PLATFORMS
84
75
  ruby
@@ -88,6 +79,7 @@ DEPENDENCIES
88
79
  flagsmith!
89
80
  gem-release
90
81
  pry
82
+ pry-byebug
91
83
  rake
92
84
  rspec
93
85
  rubocop
data/example/Gemfile CHANGED
@@ -4,7 +4,7 @@ gem 'hanami', '~> 1.3'
4
4
  gem 'hanami-model', '~> 1.3'
5
5
  gem 'slim'
6
6
  gem 'sqlite3'
7
- gem "puma", "~> 5.6"
7
+ gem "puma", "~> 6.3"
8
8
 
9
9
  gemspec path: '../'
10
10
 
data/example/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- flagsmith (3.0.0)
4
+ flagsmith (3.1.1)
5
5
  faraday
6
6
  faraday-retry
7
7
  faraday_middleware
@@ -10,8 +10,8 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- addressable (2.8.0)
14
- public_suffix (>= 2.0.2, < 5.0)
13
+ addressable (2.8.4)
14
+ public_suffix (>= 2.0.2, < 6.0)
15
15
  ast (2.4.2)
16
16
  better_errors (2.9.1)
17
17
  coderay (>= 1.0.0)
@@ -20,13 +20,14 @@ GEM
20
20
  bigdecimal (1.4.4)
21
21
  binding_of_caller (0.8.0)
22
22
  debug_inspector (>= 0.0.1)
23
- capybara (3.32.2)
23
+ capybara (3.39.0)
24
24
  addressable
25
+ matrix
25
26
  mini_mime (>= 0.1.3)
26
27
  nokogiri (~> 1.8)
27
28
  rack (>= 1.6.0)
28
29
  rack-test (>= 0.6.3)
29
- regexp_parser (~> 1.5)
30
+ regexp_parser (>= 1.5, < 3.0)
30
31
  xpath (~> 3.2)
31
32
  coderay (1.1.3)
32
33
  concurrent-ruby (1.1.10)
@@ -71,7 +72,7 @@ GEM
71
72
  dry-logic (~> 0.4, >= 0.4.0)
72
73
  dry-types (~> 0.11.0)
73
74
  erubi (1.10.0)
74
- faraday (1.10.0)
75
+ faraday (1.10.3)
75
76
  faraday-em_http (~> 1.0)
76
77
  faraday-em_synchrony (~> 1.0)
77
78
  faraday-excon (~> 1.1)
@@ -87,8 +88,8 @@ GEM
87
88
  faraday-em_synchrony (1.0.0)
88
89
  faraday-excon (1.1.0)
89
90
  faraday-httpclient (1.0.1)
90
- faraday-multipart (1.0.3)
91
- multipart-post (>= 1.2, < 3)
91
+ faraday-multipart (1.0.4)
92
+ multipart-post (~> 2)
92
93
  faraday-net_http (1.0.1)
93
94
  faraday-net_http_persistent (1.2.0)
94
95
  faraday-patron (1.0.0)
@@ -156,28 +157,31 @@ GEM
156
157
  inflecto (0.0.2)
157
158
  mail (2.7.1)
158
159
  mini_mime (>= 0.1.1)
160
+ matrix (0.4.2)
159
161
  method_source (1.0.0)
160
162
  mini_mime (1.1.2)
161
- mini_portile2 (2.4.0)
162
- multipart-post (2.1.1)
163
- nio4r (2.5.8)
164
- nokogiri (1.10.10)
165
- mini_portile2 (~> 2.4.0)
163
+ multipart-post (2.3.0)
164
+ nio4r (2.5.9)
165
+ nokogiri (1.14.3-arm64-darwin)
166
+ racc (~> 1.4)
167
+ nokogiri (1.14.3-x86_64-linux)
168
+ racc (~> 1.4)
166
169
  parallel (1.20.1)
167
170
  parser (3.1.2.0)
168
171
  ast (~> 2.4.1)
169
172
  pry (0.14.1)
170
173
  coderay (~> 1.1)
171
174
  method_source (~> 1.0)
172
- public_suffix (4.0.7)
173
- puma (5.6.4)
175
+ public_suffix (5.0.1)
176
+ puma (6.3.1)
174
177
  nio4r (~> 2.0)
175
- rack (2.2.3.1)
176
- rack-test (1.1.0)
177
- rack (>= 1.0, < 3)
178
+ racc (1.6.2)
179
+ rack (2.2.7)
180
+ rack-test (2.1.0)
181
+ rack (>= 1.3)
178
182
  rainbow (3.1.1)
179
183
  rake (13.0.6)
180
- regexp_parser (1.8.2)
184
+ regexp_parser (2.8.0)
181
185
  rexml (3.2.5)
182
186
  rom (3.3.3)
183
187
  concurrent-ruby (~> 1.0)
@@ -245,6 +249,7 @@ GEM
245
249
  nokogiri (~> 1.8)
246
250
 
247
251
  PLATFORMS
252
+ arm64-darwin-21
248
253
  x86_64-linux
249
254
 
250
255
  DEPENDENCIES
@@ -257,7 +262,7 @@ DEPENDENCIES
257
262
  hanami-model (~> 1.3)
258
263
  hanami-webconsole
259
264
  pry
260
- puma (~> 5.6)
265
+ puma (~> 6.3)
261
266
  rake
262
267
  rspec
263
268
  rubocop
data/flagsmith.gemspec CHANGED
@@ -3,11 +3,11 @@
3
3
  require File.expand_path('lib/flagsmith/version', __dir__)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.required_ruby_version = '>= 2.4.0'
6
+ spec.required_ruby_version = '>= 3.0.0'
7
7
  spec.name = 'flagsmith'
8
8
  spec.version = Flagsmith::VERSION
9
- spec.authors = ['Tom Stuart', 'Brian Moelk']
10
- spec.email = ['tom@solidstategroup.com', 'bmoelk@gmail.com']
9
+ spec.authors = ['Tom Stuart', 'Brian Moelk', 'Zach Aysan']
10
+ spec.email = ['tom@solidstategroup.com', 'bmoelk@gmail.com', 'zachaysan@gmail.com']
11
11
  # Specify which files should be added to the gem when it is released.
12
12
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
13
  spec.files = Dir.chdir(__dir__) do
@@ -27,12 +27,13 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'bundler'
28
28
  spec.add_development_dependency 'gem-release'
29
29
  spec.add_development_dependency 'pry'
30
+ spec.add_development_dependency 'pry-byebug'
30
31
  spec.add_development_dependency 'rake'
31
32
  spec.add_development_dependency 'rspec'
32
33
  spec.add_development_dependency 'rubocop'
33
34
 
34
- spec.add_dependency 'faraday'
35
- spec.add_dependency 'faraday_middleware'
35
+ spec.add_dependency 'faraday', '>= 2.0.1'
36
36
  spec.add_dependency 'faraday-retry'
37
37
  spec.add_dependency 'semantic'
38
+ spec.metadata['rubygems_mfa_required'] = 'true'
38
39
  end
@@ -40,9 +40,7 @@ module Flagsmith
40
40
  multivariate_feature_state_values.sort.each do |multi_fs_value|
41
41
  limit = multi_fs_value.percentage_allocation + start_percentage
42
42
 
43
- if start_percentage <= percentage_value && percentage_value < limit
44
- return multi_fs_value.multivariate_feature_option.value
45
- end
43
+ return multi_fs_value.multivariate_feature_option.value if start_percentage <= percentage_value && percentage_value < limit
46
44
 
47
45
  start_percentage = limit
48
46
  end
@@ -165,7 +163,7 @@ module Flagsmith
165
163
  attr_reader :priority
166
164
 
167
165
  def initialize(params = {})
168
- @priority = params[:priority].nil? ? nil : params[:priority].to_i
166
+ @priority = params[:priority]&.to_i
169
167
  end
170
168
  end
171
169
  end
@@ -25,6 +25,7 @@ module Flagsmith
25
25
  IS_SET = 'IS_SET'
26
26
  IS_NOT_SET = 'IS_NOT_SET'
27
27
  MODULO = 'MODULO'
28
+ IN = 'IN'
28
29
 
29
30
  CONDITION_OPERATORS = [
30
31
  EQUAL,
@@ -37,7 +38,8 @@ module Flagsmith
37
38
  NOT_EQUAL,
38
39
  REGEX,
39
40
  PERCENTAGE_SPLIT,
40
- MODULO
41
+ MODULO,
42
+ IN
41
43
  ].freeze
42
44
  end
43
45
  end
@@ -36,6 +36,7 @@ module Flagsmith
36
36
  end
37
37
  end
38
38
 
39
+ # rubocop:disable Metrics/MethodLength
39
40
  def traits_match_segment_rule(identity_traits, rule, segment_id, identity_id)
40
41
  matching_block = lambda { |condition|
41
42
  traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
@@ -44,23 +45,25 @@ module Flagsmith
44
45
  matches_conditions =
45
46
  if rule.conditions&.length&.positive?
46
47
  rule.conditions.send(rule.matching_function, &matching_block)
47
- else true
48
+ else
49
+ true
48
50
  end
49
51
 
50
52
  matches_conditions &&
51
53
  rule.rules.all? { |r| traits_match_segment_rule(identity_traits, r, segment_id, identity_id) }
52
54
  end
55
+ # rubocop:enable Metrics/MethodLength
53
56
 
54
57
  def traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
55
58
  if condition.operator == PERCENTAGE_SPLIT
56
- return hashed_percentage_for_object_ids([segment_id, identity_id]) <= condition.value.to_f
59
+ return hashed_percentage_for_object_ids([segment_id,
60
+ identity_id]) <= condition.value.to_f
57
61
  end
58
62
 
59
63
  trait = identity_traits.find { |t| t.key.to_s == condition.property }
60
64
 
61
- if [IS_SET, IS_NOT_SET].include?(condition.operator)
62
- return handle_trait_existence_conditions(trait, condition.operator)
63
- end
65
+ return handle_trait_existence_conditions(trait, condition.operator) if [IS_SET,
66
+ IS_NOT_SET].include?(condition.operator)
64
67
 
65
68
  return condition.match_trait_value?(trait.trait_value) if trait
66
69
 
@@ -56,10 +56,9 @@ module Flagsmith
56
56
 
57
57
  def match_trait_value?(trait_value)
58
58
  # handle some exceptions
59
- if @value.is_a?(String) && @value.match?(/:semver$/)
60
- trait_value = Semantic::Version.new(trait_value.gsub(/:semver$/, ''))
61
- end
59
+ trait_value = Semantic::Version.new(trait_value.gsub(/:semver$/, '')) if @value.is_a?(String) && @value.match?(/:semver$/)
62
60
 
61
+ return match_in_value(trait_value) if @operator == IN
63
62
  return match_modulo_value(trait_value) if @operator == MODULO
64
63
 
65
64
  type_as_trait_value = format_to_type_of(trait_value)
@@ -73,8 +72,8 @@ module Flagsmith
73
72
  {
74
73
  'String' => ->(v) { v.to_s },
75
74
  'Semantic::Version' => ->(v) { Semantic::Version.new(v.to_s.gsub(/:semver$/, '')) },
76
- 'TrueClass' => ->(v) { ['True', 'true', 'TRUE', true, 1, '1'].include?(v) ? true : false },
77
- 'FalseClass' => ->(v) { ['False', 'false', 'FALSE', false, 0, '0'].include?(v) ? false : true },
75
+ 'TrueClass' => ->(v) { ['True', 'true', 'TRUE', true, 1, '1'].include?(v) },
76
+ 'FalseClass' => ->(v) { !['False', 'false', 'FALSE', false, 0, '0'].include?(v) },
78
77
  'Integer' => ->(v) { v.to_i },
79
78
  'Float' => ->(v) { v.to_f }
80
79
  }[input.class.to_s]
@@ -83,11 +82,17 @@ module Flagsmith
83
82
 
84
83
  def match_modulo_value(trait_value)
85
84
  divisor, remainder = @value.split('|')
86
- trait_value.is_a?(Numeric) && trait_value % divisor.to_f == remainder.to_f
85
+ trait_value.is_a?(Numeric) && trait_value % divisor.to_f == remainder.to_f # rubocop:disable Lint/FloatComparison
87
86
  rescue StandardError
88
87
  false
89
88
  end
90
89
 
90
+ def match_in_value(trait_value)
91
+ return @value.split(',').include?(trait_value.to_s) if trait_value.is_a?(String) || trait_value.is_a?(Integer)
92
+
93
+ false
94
+ end
95
+
91
96
  class << self
92
97
  def build(json)
93
98
  new(**json.slice(:operator, :value).merge(property: json[:property_]))
@@ -6,7 +6,8 @@ module Flagsmith
6
6
  DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/'
7
7
  OPTIONS = %i[
8
8
  environment_key api_url custom_headers request_timeout_seconds enable_local_evaluation
9
- environment_refresh_interval_seconds retries enable_analytics default_flag_handler logger
9
+ environment_refresh_interval_seconds retries enable_analytics default_flag_handler
10
+ offline_mode offline_handler logger
10
11
  ].freeze
11
12
 
12
13
  # Available Configs
@@ -31,8 +32,12 @@ module Flagsmith
31
32
  # API to power flag analytics charts
32
33
  # +default_flag_handler+ - ruby block which will be used in the case where
33
34
  # flags cannot be retrieved from the API or
34
- # a non existent feature is requested.
35
+ # a non-existent feature is requested.
35
36
  # The searched feature#name will be passed to the block as an argument.
37
+ # +offline_mode+ - if enabled, uses a locally provided file and
38
+ # bypasses requests to the api.
39
+ # +offline_handler+ - A file object that contains a JSON serialization of
40
+ # the entire environment, project, flags, etc.
36
41
  # +logger+ - Pass your logger, default is Logger.new($stdout)
37
42
  #
38
43
  attr_reader(*OPTIONS)
@@ -51,6 +56,10 @@ module Flagsmith
51
56
  @enable_analytics
52
57
  end
53
58
 
59
+ def offline_mode?
60
+ @offline_mode
61
+ end
62
+
54
63
  def environment_flags_url
55
64
  'flags/'
56
65
  end
@@ -78,13 +87,15 @@ module Flagsmith
78
87
  @environment_refresh_interval_seconds = opts.fetch(:environment_refresh_interval_seconds, 60)
79
88
  @enable_analytics = opts.fetch(:enable_analytics, false)
80
89
  @default_flag_handler = opts[:default_flag_handler]
90
+ @offline_mode = opts.fetch(:offline_mode, false)
91
+ @offline_handler = opts[:offline_handler]
81
92
  @logger = options.fetch(:logger, Logger.new($stdout).tap { |l| l.level = :debug })
82
93
  end
83
94
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
84
95
 
85
96
  class << self
86
97
  def environment_key
87
- ENV['FLAGSMITH_ENVIRONMENT_KEY']
98
+ ENV.fetch('FLAGSMITH_ENVIRONMENT_KEY', nil)
88
99
  end
89
100
  end
90
101
  end
@@ -4,32 +4,34 @@ module Flagsmith
4
4
  module Flags
5
5
  class NotFound < StandardError; end
6
6
 
7
+ # Base data class for the flag entity
7
8
  class BaseFlag
8
9
  include Comparable
9
10
 
10
11
  attr_reader :enabled, :value, :default
11
-
12
+
12
13
  def initialize(enabled:, value:, default:)
13
14
  @enabled = enabled
14
15
  @value = value
15
16
  @default = default
16
17
  end
17
-
18
+
18
19
  def enabled?
19
20
  enabled
20
21
  end
21
-
22
+
22
23
  alias is_default default
23
24
  end
24
25
 
26
+ # Flag class to be used by default handler logic
25
27
  class DefaultFlag < BaseFlag
26
28
  def initialize(enabled:, value:)
27
29
  super(enabled: enabled, value: value, default: true)
28
30
  end
29
31
  end
30
32
 
33
+ # 'live' Flag class as returned by API or local evaluation
31
34
  class Flag < BaseFlag
32
-
33
35
  attr_reader :feature_name, :feature_id
34
36
 
35
37
  def initialize(feature_name:, enabled:, value:, feature_id:)
@@ -77,15 +79,18 @@ module Flagsmith
77
79
  end
78
80
  end
79
81
 
82
+ # Implementation of a class to hold a collection of flags.
83
+ # Implements methods for working with the list to avoid requesting flags for each feature evaluation.
80
84
  class Collection
81
85
  include Enumerable
82
86
 
83
- attr_reader :flags, :default_flag_handler, :analytics_processor
87
+ attr_reader :flags, :default_flag_handler, :analytics_processor, :offline_handler
84
88
 
85
- def initialize(flags = {}, analytics_processor: nil, default_flag_handler: nil)
89
+ def initialize(flags = {}, analytics_processor: nil, default_flag_handler: nil, offline_handler: nil)
86
90
  @flags = flags
87
91
  @default_flag_handler = default_flag_handler
88
92
  @analytics_processor = analytics_processor
93
+ @offline_handler = offline_handler
89
94
  end
90
95
 
91
96
  def each(&block)
@@ -115,16 +120,27 @@ module Flagsmith
115
120
  end
116
121
  alias get_feature_value feature_value
117
122
 
123
+ def get_flag_from_offline_handler(key)
124
+ @offline_handler.environment.feature_states.each do |feature_state|
125
+ return Flag.from_feature_state_model(feature_state, nil) if key == Flagsmith::Flags::Collection.normalize_key(feature_state.feature.name)
126
+ end
127
+ raise Flagsmith::Flags::NotFound,
128
+ "Feature does not exist: #{key}, offline_handler did not find a flag in this case."
129
+ end
130
+
118
131
  # Get a specific flag given the feature name.
119
132
  # :param feature_name: the name of the feature to retrieve the flag for.
120
133
  # :return: BaseFlag object.
121
134
  # :raises FlagsmithClientError: if feature doesn't exist
122
135
  def get_flag(feature_name)
123
136
  key = Flagsmith::Flags::Collection.normalize_key(feature_name)
137
+
124
138
  flag = flags.fetch(key)
125
139
  @analytics_processor.track_feature(flag.feature_name) if @analytics_processor && flag.feature_id
126
140
  flag
127
141
  rescue KeyError
142
+ return get_flag_from_offline_handler(key) if @offline_handler
143
+
128
144
  return @default_flag_handler.call(feature_name) if @default_flag_handler
129
145
 
130
146
  raise Flagsmith::Flags::NotFound,
@@ -159,7 +175,7 @@ module Flagsmith
159
175
  def from_feature_state_models(feature_states, identity_id: nil, **args)
160
176
  to_flag_object = lambda { |feature_state, acc|
161
177
  acc[normalize_key(feature_state.feature.name)] =
162
- Flagsmith::Flags::Flag.from_feature_state_model(feature_state, identity_id)
178
+ Flagsmith::Flags::Flag.from_feature_state_model(feature_state, identity_id)
163
179
  }
164
180
 
165
181
  new(
@@ -2,9 +2,10 @@
2
2
 
3
3
  module Flagsmith
4
4
  module Segments
5
+ # Data class to hold segment information.
5
6
  class Segment
6
7
  attr_reader :id, :name
7
-
8
+
8
9
  def initialize(id:, name:)
9
10
  @id = id
10
11
  @name = name
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ module OfflineHandlers
5
+ # Provides the offline_handler to the Flagsmith::Client.
6
+ class LocalFileHandler
7
+ attr_reader :environment
8
+
9
+ def initialize(environment_document_path)
10
+ environment_file = File.open(environment_document_path)
11
+
12
+ data = JSON.parse(environment_file.read, symbolize_names: true)
13
+ @environment = Flagsmith::Engine::Environment.build(data)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flagsmith
4
- VERSION = '3.1.1'
4
+ VERSION = '4.0.0'
5
5
  end
data/lib/flagsmith.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pry'
4
+ require 'pry-byebug'
5
+
3
6
  require 'faraday'
4
7
  require 'faraday/retry'
5
- require 'faraday_middleware'
6
8
 
7
9
  # Hash#slice was added in ruby version 2.5
8
10
  # This is the patch to use slice in earler versions
@@ -16,13 +18,14 @@ require 'flagsmith/sdk/intervals'
16
18
  require 'flagsmith/sdk/pooling_manager'
17
19
  require 'flagsmith/sdk/models/flags'
18
20
  require 'flagsmith/sdk/models/segments'
21
+ require 'flagsmith/sdk/offline_handlers'
19
22
 
20
23
  require 'flagsmith/engine/core'
21
24
 
22
25
  # no-doc
23
26
  module Flagsmith
24
27
  # Ruby client for flagsmith.com
25
- class Client
28
+ class Client # rubocop:disable Metrics/ClassLength
26
29
  extend Forwardable
27
30
  # A Flagsmith client.
28
31
  #
@@ -44,7 +47,9 @@ module Flagsmith
44
47
  # Available Configs.
45
48
  #
46
49
  # :environment_key, :api_url, :custom_headers, :request_timeout_seconds, :enable_local_evaluation,
47
- # :environment_refresh_interval_seconds, :retries, :enable_analytics, :default_flag_handler
50
+ # :environment_refresh_interval_seconds, :retries, :enable_analytics, :default_flag_handler,
51
+ # :offline_mode, :offline_handler
52
+ #
48
53
  # You can see full description in the Flagsmith::Config
49
54
 
50
55
  attr_reader :config, :environment
@@ -55,10 +60,24 @@ module Flagsmith
55
60
  @_mutex = Mutex.new
56
61
  @config = Flagsmith::Config.new(config)
57
62
 
63
+ validate_offline_mode!
64
+
58
65
  api_client
59
66
  analytics_processor
60
67
  environment_data_polling_manager
61
68
  engine
69
+ load_offline_handler
70
+ end
71
+
72
+ def validate_offline_mode!
73
+ if @config.offline_mode? && !@config.offline_handler
74
+ raise Flagsmith::ClientError,
75
+ 'The offline_mode config param requires a matching offline_handler.'
76
+ end
77
+ return unless @config.offline_handler && @config.default_flag_handler
78
+
79
+ raise Flagsmith::ClientError,
80
+ 'Cannot use offline_handler and default_flag_handler at the same time.'
62
81
  end
63
82
 
64
83
  def api_client
@@ -79,6 +98,10 @@ module Flagsmith
79
98
  )
80
99
  end
81
100
 
101
+ def load_offline_handler
102
+ @environment = offline_handler.environment if offline_handler
103
+ end
104
+
82
105
  def environment_data_polling_manager
83
106
  return nil unless @config.local_evaluation?
84
107
 
@@ -103,7 +126,7 @@ module Flagsmith
103
126
  # Get all the default for flags for the current environment.
104
127
  # @returns Flags object holding all the flags for the current environment.
105
128
  def get_environment_flags # rubocop:disable Naming/AccessorMethodName
106
- return environment_flags_from_document if @config.local_evaluation?
129
+ return environment_flags_from_document if @config.local_evaluation? || @config.offline_mode
107
130
 
108
131
  environment_flags_from_api
109
132
  end
@@ -153,13 +176,13 @@ module Flagsmith
153
176
 
154
177
  def get_identity_segments(identifier, traits = {})
155
178
  unless environment
156
- raise Flagsmith::ClientError,
157
- 'Local evaluation required to obtain identity segments.'
179
+ raise Flagsmith::ClientError,
180
+ 'Local evaluation or offline handler is required to obtain identity segments.'
158
181
  end
159
182
 
160
183
  identity_model = build_identity_model(identifier, traits)
161
184
  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
185
+ segment_models.map { |sm| Flagsmith::Segments::Segment.new(id: sm.id, name: sm.name) }.compact
163
186
  end
164
187
 
165
188
  private
@@ -168,7 +191,8 @@ module Flagsmith
168
191
  Flagsmith::Flags::Collection.from_feature_state_models(
169
192
  engine.get_environment_feature_states(environment),
170
193
  analytics_processor: analytics_processor,
171
- default_flag_handler: default_flag_handler
194
+ default_flag_handler: default_flag_handler,
195
+ offline_handler: offline_handler
172
196
  )
173
197
  end
174
198
 
@@ -178,45 +202,80 @@ module Flagsmith
178
202
  Flagsmith::Flags::Collection.from_feature_state_models(
179
203
  engine.get_identity_feature_states(environment, identity_model),
180
204
  analytics_processor: analytics_processor,
181
- default_flag_handler: default_flag_handler
205
+ default_flag_handler: default_flag_handler,
206
+ offline_handler: offline_handler
182
207
  )
183
208
  end
184
209
 
210
+ # rubocop:disable Metrics/MethodLength
185
211
  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
- )
212
+ if offline_handler
213
+ begin
214
+ process_environment_flags_from_api
215
+ rescue StandardError
216
+ environment_flags_from_document
217
+ end
218
+ else
219
+ begin
220
+ process_environment_flags_from_api
221
+ rescue StandardError
222
+ if default_flag_handler
223
+ return Flagsmith::Flags::Collection.new(
224
+ {},
225
+ default_flag_handler: default_flag_handler
226
+ )
227
+ end
228
+ raise
229
+ end
194
230
  end
195
231
  end
232
+ # rubocop:enable Metrics/MethodLength
233
+
234
+ def process_environment_flags_from_api
235
+ api_flags = api_client.get(@config.environment_flags_url).body
236
+ api_flags = api_flags.select { |flag| flag[:feature_segment].nil? }
237
+ Flagsmith::Flags::Collection.from_api(
238
+ api_flags,
239
+ analytics_processor: analytics_processor,
240
+ default_flag_handler: default_flag_handler,
241
+ offline_handler: offline_handler
242
+ )
243
+ end
196
244
 
245
+ # rubocop:disable Metrics/MethodLength
197
246
  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
- )
247
+ if offline_handler
248
+ begin
249
+ process_identity_flags_from_api(identifier, traits)
250
+ rescue StandardError
251
+ get_identity_flags_from_document(identifier, traits)
252
+ end
253
+ else
254
+ begin
255
+ process_identity_flags_from_api(identifier, traits)
256
+ rescue StandardError
257
+ if default_flag_handler
258
+ return Flagsmith::Flags::Collection.new(
259
+ {},
260
+ default_flag_handler: default_flag_handler
261
+ )
262
+ end
263
+ raise
264
+ end
207
265
  end
208
266
  end
267
+ # rubocop:enable Metrics/MethodLength
209
268
 
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
269
+ def process_identity_flags_from_api(identifier, traits = {})
270
+ data = generate_identities_data(identifier, traits)
271
+ json_response = api_client.post(@config.identities_url, data.to_json).body
272
+
273
+ Flagsmith::Flags::Collection.from_api(
274
+ json_response[:flags],
275
+ analytics_processor: analytics_processor,
276
+ default_flag_handler: default_flag_handler,
277
+ offline_handler: offline_handler
278
+ )
220
279
  end
221
280
 
222
281
  def build_identity_model(identifier, traits = {})
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flagsmith
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Stuart
8
8
  - Brian Moelk
9
- autorequire:
9
+ - Zach Aysan
10
+ autorequire:
10
11
  bindir: exe
11
12
  cert_chain: []
12
- date: 2023-02-22 00:00:00.000000000 Z
13
+ date: 2024-01-30 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: bundler
@@ -54,7 +55,7 @@ dependencies:
54
55
  - !ruby/object:Gem::Version
55
56
  version: '0'
56
57
  - !ruby/object:Gem::Dependency
57
- name: rake
58
+ name: pry-byebug
58
59
  requirement: !ruby/object:Gem::Requirement
59
60
  requirements:
60
61
  - - ">="
@@ -68,7 +69,7 @@ dependencies:
68
69
  - !ruby/object:Gem::Version
69
70
  version: '0'
70
71
  - !ruby/object:Gem::Dependency
71
- name: rspec
72
+ name: rake
72
73
  requirement: !ruby/object:Gem::Requirement
73
74
  requirements:
74
75
  - - ">="
@@ -82,7 +83,7 @@ dependencies:
82
83
  - !ruby/object:Gem::Version
83
84
  version: '0'
84
85
  - !ruby/object:Gem::Dependency
85
- name: rubocop
86
+ name: rspec
86
87
  requirement: !ruby/object:Gem::Requirement
87
88
  requirements:
88
89
  - - ">="
@@ -96,13 +97,13 @@ dependencies:
96
97
  - !ruby/object:Gem::Version
97
98
  version: '0'
98
99
  - !ruby/object:Gem::Dependency
99
- name: faraday
100
+ name: rubocop
100
101
  requirement: !ruby/object:Gem::Requirement
101
102
  requirements:
102
103
  - - ">="
103
104
  - !ruby/object:Gem::Version
104
105
  version: '0'
105
- type: :runtime
106
+ type: :development
106
107
  prerelease: false
107
108
  version_requirements: !ruby/object:Gem::Requirement
108
109
  requirements:
@@ -110,19 +111,19 @@ dependencies:
110
111
  - !ruby/object:Gem::Version
111
112
  version: '0'
112
113
  - !ruby/object:Gem::Dependency
113
- name: faraday_middleware
114
+ name: faraday
114
115
  requirement: !ruby/object:Gem::Requirement
115
116
  requirements:
116
117
  - - ">="
117
118
  - !ruby/object:Gem::Version
118
- version: '0'
119
+ version: 2.0.1
119
120
  type: :runtime
120
121
  prerelease: false
121
122
  version_requirements: !ruby/object:Gem::Requirement
122
123
  requirements:
123
124
  - - ">="
124
125
  - !ruby/object:Gem::Version
125
- version: '0'
126
+ version: 2.0.1
126
127
  - !ruby/object:Gem::Dependency
127
128
  name: faraday-retry
128
129
  requirement: !ruby/object:Gem::Requirement
@@ -156,6 +157,7 @@ description: Ruby Client for Flagsmith. Ship features with confidence using feat
156
157
  email:
157
158
  - tom@solidstategroup.com
158
159
  - bmoelk@gmail.com
160
+ - zachaysan@gmail.com
159
161
  executables: []
160
162
  extensions: []
161
163
  extra_rdoc_files: []
@@ -224,12 +226,14 @@ files:
224
226
  - lib/flagsmith/sdk/intervals.rb
225
227
  - lib/flagsmith/sdk/models/flags.rb
226
228
  - lib/flagsmith/sdk/models/segments.rb
229
+ - lib/flagsmith/sdk/offline_handlers.rb
227
230
  - lib/flagsmith/sdk/pooling_manager.rb
228
231
  - lib/flagsmith/version.rb
229
232
  homepage: https://flagsmith.com
230
233
  licenses: []
231
- metadata: {}
232
- post_install_message:
234
+ metadata:
235
+ rubygems_mfa_required: 'true'
236
+ post_install_message:
233
237
  rdoc_options: []
234
238
  require_paths:
235
239
  - lib
@@ -237,15 +241,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
237
241
  requirements:
238
242
  - - ">="
239
243
  - !ruby/object:Gem::Version
240
- version: 2.4.0
244
+ version: 3.0.0
241
245
  required_rubygems_version: !ruby/object:Gem::Requirement
242
246
  requirements:
243
247
  - - ">="
244
248
  - !ruby/object:Gem::Version
245
249
  version: '0'
246
250
  requirements: []
247
- rubygems_version: 3.3.7
248
- signing_key:
251
+ rubygems_version: 3.3.26
252
+ signing_key:
249
253
  specification_version: 4
250
254
  summary: Flagsmith - Ship features with confidence
251
255
  test_files: []