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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +74 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +14 -60
- data/.ruby-version +1 -1
- data/.vscode/settings.json +5 -1
- data/Gemfile +11 -1
- data/Gemfile.lock +80 -68
- data/README.md +53 -2
- data/Rakefile +5 -5
- data/bin/tapioca +27 -0
- data/bin/test +8 -0
- data/lib/example_files/config/initializers/simple_feature_flags.rb +4 -3
- data/lib/simple_feature_flags/base_storage.rb +296 -0
- data/lib/simple_feature_flags/cli/command/generate.rb +33 -6
- data/lib/simple_feature_flags/cli/command.rb +3 -1
- data/lib/simple_feature_flags/cli/options.rb +19 -3
- data/lib/simple_feature_flags/cli/runner.rb +13 -5
- data/lib/simple_feature_flags/cli.rb +3 -1
- data/lib/simple_feature_flags/configuration.rb +6 -0
- data/lib/simple_feature_flags/ram_storage.rb +275 -61
- data/lib/simple_feature_flags/redis_storage.rb +265 -44
- data/lib/simple_feature_flags/test_ram_storage.rb +7 -1
- data/lib/simple_feature_flags/version.rb +1 -1
- data/lib/simple_feature_flags.rb +22 -9
- data/simple_feature_flags.gemspec +17 -22
- metadata +19 -125
- data/.travis.yml +0 -6
@@ -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
|
-
|
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
|
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.
|
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.
|
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.
|
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
|
-
|
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__)].
|
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
|
-
|
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 =
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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__)].
|
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
|