simple_feature_flags 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,296 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaml'
5
+
6
+ module SimpleFeatureFlags
7
+ # Abstract class for all storage adapters.
8
+ class BaseStorage
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ abstract!
13
+
14
+ # Path to the file with feature flags
15
+ sig { abstract.returns(String) }
16
+ def file; end
17
+
18
+ sig { abstract.returns(T::Array[String]) }
19
+ def mandatory_flags; end
20
+
21
+ # Checks whether the flag is active. Returns `true`, `false`, `:globally` or `:partially`
22
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T.any(Symbol, T::Boolean)) }
23
+ def active(feature); end
24
+
25
+ # Checks whether the flag is active.
26
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
27
+ def active?(feature); end
28
+
29
+ # Checks whether the flag is inactive.
30
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
31
+ def inactive?(feature); end
32
+
33
+ # Checks whether the flag is active globally, for every object.
34
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
35
+ def active_globally?(feature); end
36
+
37
+ # Checks whether the flag is inactive globally, for every object.
38
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
39
+ def inactive_globally?(feature); end
40
+
41
+ # Checks whether the flag is active partially, only for certain objects.
42
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
43
+ def active_partially?(feature); end
44
+
45
+ # Checks whether the flag is inactive partially, only for certain objects.
46
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
47
+ def inactive_partially?(feature); end
48
+
49
+ # Checks whether the flag is active for the given object.
50
+ sig do
51
+ abstract
52
+ .params(
53
+ feature: T.any(Symbol, String),
54
+ object: Object,
55
+ object_id_method: Symbol,
56
+ )
57
+ .returns(T::Boolean)
58
+ end
59
+ def active_for?(feature, object, object_id_method: :id); end
60
+
61
+ # Checks whether the flag is inactive for the given object.
62
+ sig do
63
+ abstract
64
+ .params(
65
+ feature: T.any(Symbol, String),
66
+ object: Object,
67
+ object_id_method: Symbol,
68
+ )
69
+ .returns(T::Boolean)
70
+ end
71
+ def inactive_for?(feature, object, object_id_method: :id); end
72
+
73
+ # Checks whether the flag exists.
74
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
75
+ def exists?(feature); end
76
+
77
+ # Returns the description of the flag if it exists.
78
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T.nilable(String)) }
79
+ def description(feature); end
80
+
81
+ # Calls the given block if the flag is active.
82
+ sig do
83
+ abstract
84
+ .params(
85
+ feature: T.any(Symbol, String),
86
+ block: T.proc.void,
87
+ ).void
88
+ end
89
+ def when_active(feature, &block); end
90
+
91
+ # Calls the given block if the flag is inactive.
92
+ sig do
93
+ abstract
94
+ .params(
95
+ feature: T.any(Symbol, String),
96
+ block: T.proc.void,
97
+ ).void
98
+ end
99
+ def when_inactive(feature, &block); end
100
+
101
+ # Calls the given block if the flag is active globally.
102
+ sig do
103
+ abstract
104
+ .params(
105
+ feature: T.any(Symbol, String),
106
+ block: T.proc.void,
107
+ ).void
108
+ end
109
+ def when_active_globally(feature, &block); end
110
+
111
+ # Calls the given block if the flag is inactive globally.
112
+ sig do
113
+ abstract
114
+ .params(
115
+ feature: T.any(Symbol, String),
116
+ block: T.proc.void,
117
+ ).void
118
+ end
119
+ def when_inactive_globally(feature, &block); end
120
+
121
+ # Calls the given block if the flag is active partially.
122
+ sig do
123
+ abstract
124
+ .params(
125
+ feature: T.any(Symbol, String),
126
+ block: T.proc.void,
127
+ ).void
128
+ end
129
+ def when_active_partially(feature, &block); end
130
+
131
+ # Calls the given block if the flag is inactive partially.
132
+ sig do
133
+ abstract
134
+ .params(
135
+ feature: T.any(Symbol, String),
136
+ block: T.proc.void,
137
+ ).void
138
+ end
139
+ def when_inactive_partially(feature, &block); end
140
+
141
+ # Calls the given block if the flag is active for the given object.
142
+ sig do
143
+ abstract
144
+ .params(
145
+ feature: T.any(Symbol, String),
146
+ object: Object,
147
+ object_id_method: Symbol,
148
+ block: T.proc.void,
149
+ ).void
150
+ end
151
+ def when_active_for(feature, object, object_id_method: CONFIG.default_id_method, &block); end
152
+
153
+ # Calls the given block if the flag is inactive for the given object.
154
+ sig do
155
+ abstract
156
+ .params(
157
+ feature: T.any(Symbol, String),
158
+ object: Object,
159
+ object_id_method: Symbol,
160
+ block: T.proc.void,
161
+ ).void
162
+ end
163
+ def when_inactive_for(feature, object, object_id_method: CONFIG.default_id_method, &block); end
164
+
165
+ # Activates the given flag. Returns `false` if it does not exist.
166
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
167
+ def activate(feature); end
168
+
169
+ # Activates the given flag globally. Returns `false` if it does not exist.
170
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
171
+ def activate_globally(feature); end
172
+
173
+ # Activates the given flag partially. Returns `false` if it does not exist.
174
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
175
+ def activate_partially(feature); end
176
+
177
+ # Activates the given flag for the given objects. Returns `false` if it does not exist.
178
+ sig do
179
+ abstract
180
+ .params(
181
+ feature: T.any(Symbol, String),
182
+ objects: Object,
183
+ object_id_method: Symbol,
184
+ ).void
185
+ end
186
+ def activate_for(feature, *objects, object_id_method: CONFIG.default_id_method); end
187
+
188
+ # Activates the given flag for the given objects and sets the flag as partially active.
189
+ # Returns `false` if it does not exist.
190
+ sig do
191
+ abstract
192
+ .params(
193
+ feature: T.any(Symbol, String),
194
+ objects: Object,
195
+ object_id_method: Symbol,
196
+ ).void
197
+ end
198
+ def activate_for!(feature, *objects, object_id_method: CONFIG.default_id_method); end
199
+
200
+ # Deactivates the given flag for all objects.
201
+ # Resets the list of objects that this flag has been turned on for.
202
+ # Returns `false` if it does not exist.
203
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
204
+ def deactivate!(feature); end
205
+
206
+ # Deactivates the given flag globally.
207
+ # Does not reset the list of objects that this flag has been turned on for.
208
+ # Returns `false` if it does not exist.
209
+ sig { abstract.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
210
+ def deactivate(feature); end
211
+
212
+ # Returns a hash of Objects that the given flag is turned on for.
213
+ # The keys are class/model names, values are arrays of IDs of instances/records.
214
+ #
215
+ # looks like this:
216
+ #
217
+ # { "Page" => [25, 89], "Book" => [152] }
218
+ #
219
+ sig do
220
+ abstract
221
+ .params(feature: T.any(Symbol, String))
222
+ .returns(T::Hash[String, T::Array[Object]])
223
+ end
224
+ def active_objects(feature); end
225
+
226
+ # Deactivates the given flag for the given objects. Returns `false` if it does not exist.
227
+ sig do
228
+ abstract
229
+ .params(
230
+ feature: T.any(Symbol, String),
231
+ objects: Object,
232
+ object_id_method: Symbol,
233
+ ).void
234
+ end
235
+ def deactivate_for(feature, *objects, object_id_method: CONFIG.default_id_method); end
236
+
237
+ # Returns the data of the flag in a hash.
238
+ sig do
239
+ abstract
240
+ .params(
241
+ feature: T.any(Symbol, String),
242
+ ).returns(T.nilable(T::Hash[String, T.anything]))
243
+ end
244
+ def get(feature); end
245
+
246
+ # Adds the given feature flag.
247
+ sig do
248
+ abstract
249
+ .params(
250
+ feature: T.any(Symbol, String),
251
+ description: String,
252
+ active: T.any(String, Symbol, T::Boolean, NilClass),
253
+ ).returns(T.nilable(T::Hash[String, T.anything]))
254
+ end
255
+ def add(feature, description, active = 'false'); end
256
+
257
+ # Removes the given feature flag.
258
+ # Returns its data or nil if it does not exist.
259
+ sig do
260
+ abstract
261
+ .params(
262
+ feature: T.any(Symbol, String),
263
+ ).returns(T.nilable(T::Hash[String, T.anything]))
264
+ end
265
+ def remove(feature); end
266
+
267
+ # Returns the data of all feature flags.
268
+ sig do
269
+ abstract.returns(T::Array[T::Hash[String, T.anything]])
270
+ end
271
+ def all; end
272
+
273
+ private
274
+
275
+ sig { params(objects: T::Array[Object], object_id_method: Symbol).returns(T::Hash[String, T::Array[Object]]) }
276
+ def objects_to_hash(objects, object_id_method: CONFIG.default_id_method)
277
+ objects.group_by { |ob| ob.class.to_s }
278
+ .transform_values { |arr| arr.map(&object_id_method) }
279
+ end
280
+
281
+ sig { void }
282
+ def import_flags_from_file
283
+ changes = YAML.load_file(file)
284
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
285
+
286
+ changes[:mandatory].each do |el|
287
+ mandatory_flags << el['name']
288
+ add(el['name'], el['description'], el['active'])
289
+ end
290
+
291
+ changes[:remove].each do |el|
292
+ remove(el)
293
+ end
294
+ end
295
+ end
296
+ end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'fileutils'
@@ -5,15 +6,21 @@ require 'fileutils'
5
6
  module SimpleFeatureFlags
6
7
  module Cli
7
8
  module Command
9
+ # Implements the `generate` CLI command
8
10
  class Generate
9
- CONFIG_FILE = 'simple_feature_flags.yml'
11
+ extend T::Sig
10
12
 
13
+ CONFIG_FILE = T.let('simple_feature_flags.yml', String)
14
+
15
+ sig { returns(Options) }
11
16
  attr_reader :options
12
17
 
18
+ sig { params(options: Options).void }
13
19
  def initialize(options)
14
20
  @options = options
15
21
  end
16
22
 
23
+ sig { void }
17
24
  def run
18
25
  if options.rails
19
26
  generate_for_rails
@@ -29,10 +36,11 @@ module SimpleFeatureFlags
29
36
 
30
37
  private
31
38
 
39
+ sig { void }
32
40
  def generate_for_rails
33
41
  ::FileUtils.cp_r example_config_dir, destination_dir
34
42
 
35
- puts "Generated:"
43
+ puts 'Generated:'
36
44
  puts '----------'
37
45
  puts "- #{::File.join(destination_dir, 'config')}"
38
46
  print_dir_tree(example_config_dir, 1)
@@ -62,21 +70,30 @@ module SimpleFeatureFlags
62
70
  system 'bundle'
63
71
  end
64
72
 
73
+ sig do
74
+ params(
75
+ file_path: String,
76
+ regexp: Regexp,
77
+ block: T.proc.params(arg0: String).returns(String),
78
+ ).void
79
+ end
65
80
  def file_gsub(file_path, regexp, &block)
66
81
  new_content = File.read(file_path).gsub(regexp, &block)
67
- File.open(file_path, 'wb') { |file| file.write(new_content) }
82
+ File.binwrite(file_path, new_content)
68
83
  end
69
84
 
85
+ sig { params(file_path: String, line: String).void }
70
86
  def file_append(file_path, line)
71
87
  new_content = File.read(file_path)
72
88
  new_content = "#{new_content}\n#{line}\n"
73
- File.open(file_path, 'wb') { |file| file.write(new_content) }
89
+ File.binwrite(file_path, new_content)
74
90
  end
75
91
 
92
+ sig { params(dir: String, embed_level: Integer).void }
76
93
  def print_dir_tree(dir, embed_level = 0)
77
94
  padding = ' ' * (embed_level * 2)
78
95
 
79
- children = ::Dir.new(dir).entries.reject { |el| /^\.{1,2}$/ =~ el }
96
+ children = ::Dir.new(dir).entries.grep_v(/^\.{1,2}$/)
80
97
 
81
98
  children.each do |child|
82
99
  child_dir = ::File.join(dir, child)
@@ -88,32 +105,42 @@ module SimpleFeatureFlags
88
105
  end
89
106
  end
90
107
 
108
+ sig { returns String }
91
109
  def initializer_file
92
110
  ::File.join(destination_dir, 'config', 'initializers', 'simple_feature_flags.rb')
93
111
  end
94
112
 
113
+ sig { returns String }
95
114
  def gemfile
96
115
  ::File.join(destination_dir, 'Gemfile')
97
116
  end
98
117
 
118
+ sig { returns String }
99
119
  def routes_rb
100
120
  ::File.join(destination_dir, 'config', 'routes.rb')
101
121
  end
102
122
 
123
+ sig { returns String }
103
124
  def example_config_dir
104
125
  ::File.join(::File.expand_path(__dir__), '..', '..', '..', 'example_files', 'config')
105
126
  end
106
127
 
128
+ sig { returns String }
107
129
  def example_config_file
108
130
  ::File.join(example_config_dir, CONFIG_FILE)
109
131
  end
110
132
 
133
+ sig { returns String }
111
134
  def destination_dir
112
- raise IncorrectWorkingDirectoryError, "You should enter the main directory of your Rails project!" if options.rails && !::Dir.new(::Dir.pwd).entries.include?('config')
135
+ if options.rails && !::Dir.new(::Dir.pwd).entries.include?('config')
136
+ raise IncorrectWorkingDirectoryError,
137
+ 'You should enter the main directory of your Rails project!'
138
+ end
113
139
 
114
140
  ::Dir.pwd
115
141
  end
116
142
 
143
+ sig { returns String }
117
144
  def destination_file
118
145
  @destination_file ||= ::File.join(destination_dir, CONFIG_FILE)
119
146
  end
@@ -1,9 +1,11 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module SimpleFeatureFlags
4
5
  module Cli
6
+ # Contains CLI commands
5
7
  module Command; end
6
8
  end
7
9
  end
8
10
 
9
- Dir[File.expand_path('command/*.rb', __dir__)].sort.each { |file| require file }
11
+ Dir[File.expand_path('command/*.rb', __dir__)].each { |file| require file }
@@ -1,15 +1,31 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'optparse'
4
5
 
5
6
  module SimpleFeatureFlags
6
7
  module Cli
8
+ # Parses CLI options.
7
9
  class Options
8
- attr_reader :opt_parser, :generate, :help, :rails, :ui
10
+ extend T::Sig
9
11
 
12
+ sig { returns(OptionParser) }
13
+ attr_reader :opt_parser
14
+
15
+ sig { returns(T::Boolean) }
16
+ attr_reader :generate
17
+
18
+ sig { returns(T::Boolean) }
19
+ attr_reader :rails
20
+
21
+ sig { returns(T::Boolean) }
22
+ attr_reader :ui
23
+
24
+ sig { params(args: T::Array[String]).void }
10
25
  def initialize(args)
11
- @rails = true
12
- @ui = false
26
+ @rails = T.let(true, T::Boolean)
27
+ @ui = T.let(false, T::Boolean)
28
+ @generate = T.let(false, T::Boolean)
13
29
 
14
30
  @opt_parser = ::OptionParser.new do |opts|
15
31
  opts.banner = 'Usage: simple_feature_flags [options]'
@@ -1,20 +1,28 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module SimpleFeatureFlags
4
5
  module Cli
6
+ # Runs CLI commands
5
7
  class Runner
8
+ extend T::Sig
9
+
10
+ sig { returns(Options) }
6
11
  attr_reader :options
7
12
 
13
+ sig { params(args: T::Array[String]).void }
8
14
  def initialize(args = ARGV)
9
15
  @options = Options.new(args)
10
16
  end
11
17
 
18
+ sig { void }
12
19
  def run
13
- command_class = if @options.generate
14
- ::SimpleFeatureFlags::Cli::Command::Generate
15
- else
16
- raise NoSuchCommandError, 'No such command!'
17
- end
20
+ command_class =
21
+ if @options.generate
22
+ ::SimpleFeatureFlags::Cli::Command::Generate
23
+ else
24
+ raise NoSuchCommandError, 'No such command!'
25
+ end
18
26
 
19
27
  command_class.new(options).run
20
28
  end
@@ -1,7 +1,9 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module SimpleFeatureFlags
5
+ # Handles the CLI
4
6
  module Cli; end
5
7
  end
6
8
 
7
- Dir[File.expand_path('cli/*.rb', __dir__)].sort.each { |file| require file }
9
+ Dir[File.expand_path('cli/*.rb', __dir__)].each { |file| require file }
@@ -1,9 +1,15 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module SimpleFeatureFlags
5
+ # The main configuration object of the library.
4
6
  class Configuration
7
+ extend T::Sig
8
+
9
+ sig { returns(Symbol) }
5
10
  attr_accessor :default_id_method
6
11
 
12
+ sig { void }
7
13
  def initialize
8
14
  @default_id_method = :id
9
15
  end