flagsmith 3.1.1 → 4.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 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: []