Skip to content

Commit 5478181

Browse files
authored
Merge pull request #253 from MITLibraries/use-77-feature-flags
Initial Feature Flag system
2 parents 01dec65 + a5d6666 commit 5478181

File tree

6 files changed

+187
-0
lines changed

6 files changed

+187
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ yarn-debug.log*
3333

3434
.DS_Store
3535
.vscode/
36+
.yardoc

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ group :development do
3838
gem 'rubocop'
3939
gem 'rubocop-rails'
4040
gem 'web-console'
41+
gem 'yard'
4142
end
4243

4344
group :test do

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ GEM
392392
websocket-extensions (0.1.5)
393393
xpath (3.2.0)
394394
nokogiri (~> 1.8)
395+
yard (0.9.37)
395396
zeitwerk (2.7.3)
396397

397398
PLATFORMS
@@ -443,6 +444,7 @@ DEPENDENCIES
443444
vcr
444445
web-console
445446
webmock
447+
yard
446448

447449
RUBY VERSION
448450
ruby 3.4.7p58

Makefile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.PHONY: test
2+
3+
help: # display Makefile commands
4+
@awk 'BEGIN { FS = ":.*#"; print "Usage: make <target>\n\nTargets:" } \
5+
/^[-_[:alpha:]]+:.?*#/ { printf " %-15s%s\n", $$1, $$2 }' $(MAKEFILE_LIST)
6+
7+
#######################
8+
# Local development commands
9+
#######################
10+
11+
run: # runs server on localhost
12+
bin/rails server
13+
14+
console: # runs console
15+
bin/rails console
16+
17+
test: # Run tests
18+
bin/rails test
19+
20+
coverage: test # Run tests and open coverage report in default web browser
21+
open coverage/index.html
22+
23+
#######################
24+
# Documentation commands
25+
#######################
26+
27+
annotate: # update Rails models documentation header
28+
bundle exec annotate --models
29+
30+
docserver: # runs local documentation server
31+
rm -rf .yardoc # Clears cache as it's sketchy af
32+
yard server --reload
33+
34+
#######################
35+
# Dependency commands
36+
#######################
37+
38+
install: # Install dependencies
39+
bundle install
40+
41+
outdated: # List outdated dependencies
42+
bundle outdated
43+
44+
####################################
45+
# Code quality and safety commands
46+
####################################
47+
48+
lint:
49+
bundle exec rubocop
50+
51+
lint-models:
52+
bundle exec rubocop app/models
53+
54+
lint-controllers:
55+
bundle exec rubocop app/controllers

app/models/feature.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Central feature flag management class for the application. This class provides a simple,
2+
# centralized way to manage feature flags throughout the codebase.
3+
#
4+
# Feature flags are controlled through environment variables with the prefix 'FEATURE_'
5+
# followed by the uppercase feature name. Each flag defaults to false unless explicitly
6+
# enabled via environment variable to ensure consistency in how our features work.
7+
#
8+
# @example Basic Usage
9+
# Feature.enabled?(:geodata) # true if FEATURE_GEODATA=true in ENV
10+
# Feature.enabled?(:unknown) # Returns false for undefined features
11+
#
12+
# @example Setting Flags in Environment
13+
# # In local ENV or Heroku config:
14+
# FEATURE_GEODATA=true # Enables the GDT feature
15+
# FEATURE_BOOLEAN_PICKER=true # Enables the boolean picker feature
16+
#
17+
# # Any non-true value or not setting ENV does not enable the feature
18+
# FEATURE_GEODATA=false # Does not enable the GDT feature
19+
# FEATURE_GEODATA=1 # Does not enable the GDT feature
20+
# FEATURE_GEODATA=on # Does not enable the GDT feature
21+
#
22+
# @example Usage in Different Contexts
23+
# # In models/controllers:
24+
# return unless Feature.enabled?(:geodata)
25+
#
26+
# # In views:
27+
# <% if Feature.enabled?(:geodata) %>
28+
#
29+
# # In tests:
30+
# ClimateControl.modify FEATURE_GEODATA: 'true' do
31+
# assert Feature.enabled?(:geodata)
32+
# end
33+
#
34+
class Feature
35+
# List of all valid features in the application
36+
VALID_FEATURES = %i[geodata boolean_picker].freeze
37+
38+
# Check if a feature is enabled by name
39+
#
40+
# @param feature_name [Symbol] The name of the feature to check
41+
# @return [Boolean] true if the feature is enabled, false otherwise
42+
# @example Check if a feature is enabled
43+
# Feature.enabled?(:geodata)
44+
def self.enabled?(feature_name)
45+
return false unless VALID_FEATURES.include?(feature_name)
46+
47+
ENV.fetch("FEATURE_#{feature_name.to_s.upcase}", false).to_s.downcase == 'true'
48+
end
49+
end

test/models/feature_test.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
require 'test_helper'
2+
3+
class FeatureTest < ActiveSupport::TestCase
4+
test 'defined features default to false' do
5+
refute Feature.enabled?(:geodata)
6+
refute Feature.enabled?(:boolean_picker)
7+
end
8+
9+
test 'undefined features return false' do
10+
refute Feature.enabled?(:undefined_feature)
11+
end
12+
13+
test 'features can be enabled via environment variables' do
14+
ClimateControl.modify FEATURE_GEODATA: 'true' do
15+
assert Feature.enabled?(:geodata)
16+
end
17+
end
18+
19+
test 'features can be disabled via environment variables' do
20+
ClimateControl.modify FEATURE_GEODATA: 'false' do
21+
refute Feature.enabled?(:geodata)
22+
end
23+
end
24+
25+
test 'environment variables are case insensitive' do
26+
ClimateControl.modify FEATURE_GEODATA: 'TRUE' do
27+
assert Feature.enabled?(:geodata)
28+
end
29+
30+
ClimateControl.modify FEATURE_GEODATA: 'True' do
31+
assert Feature.enabled?(:geodata)
32+
end
33+
34+
ClimateControl.modify FEATURE_GEODATA: 'false' do
35+
refute Feature.enabled?(:geodata)
36+
end
37+
38+
ClimateControl.modify FEATURE_GEODATA: 'FALSE' do
39+
refute Feature.enabled?(:geodata)
40+
end
41+
end
42+
43+
test 'non true boolean values default to false' do
44+
ClimateControl.modify(
45+
FEATURE_GEODATA: 'invalid'
46+
) do
47+
refute Feature.enabled?(:geodata)
48+
end
49+
50+
ClimateControl.modify(
51+
FEATURE_GEODATA: '1'
52+
) do
53+
refute Feature.enabled?(:geodata)
54+
end
55+
56+
ClimateControl.modify(
57+
FEATURE_GEODATA: 'yes'
58+
) do
59+
refute Feature.enabled?(:geodata)
60+
end
61+
end
62+
63+
test 'feature names are case sensitive' do
64+
ClimateControl.modify FEATURE_GEODATA: 'true' do
65+
assert Feature.enabled?(:geodata)
66+
refute Feature.enabled?(:GDT)
67+
end
68+
end
69+
70+
test 'multiple features can be controlled independently' do
71+
ClimateControl.modify(
72+
FEATURE_GEODATA: 'true',
73+
FEATURE_BOOLEAN_PICKER: 'false'
74+
) do
75+
assert Feature.enabled?(:geodata)
76+
refute Feature.enabled?(:boolean_picker)
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)