Class: Flipper::CLI

Inherits:
OptionParser
  • Object
show all
Defined in:
lib/flipper/cli.rb

Defined Under Namespace

Modules: ShellOutput Classes: Command

Constant Summary collapse

DEFAULT_REQUIRE =

Path to the local Rails application’s environment configuration.

"./config/environment"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stdout: $stdout, stderr: $stderr, shell: Bundler::Thor::Base.shell.new) ⇒ CLI

Returns a new instance of CLI.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/flipper/cli.rb', line 14

def initialize(stdout: $stdout, stderr: $stderr, shell: Bundler::Thor::Base.shell.new)
  super

  # Program is always flipper, no matter how it's invoked
  @program_name = 'flipper'
  @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE)
  @commands = {}

  # Extend whatever shell to support output redirection
  @shell = shell.extend(ShellOutput)
  shell.redirect(stdout: stdout, stderr: stderr)

  %w[enable disable].each do |action|
    command action do |c|
      c.banner = "Usage: #{c.program_name} [options] <feature>"
      c.description = "#{action.to_s.capitalize} a feature"

      values = []

      c.on('-a id', '--actor=id', "#{action} for an actor") do |id|
        values << Actor.new(id)
      end
      c.on('-g name', '--group=name', "#{action} for a group") do |name|
        values << Types::Group.new(name)
      end
      c.on('-p NUM', '--percentage-of-actors=NUM', Numeric, "#{action} for a percentage of actors") do |num|
        values << Types::PercentageOfActors.new(num)
      end
      c.on('-t NUM', '--percentage-of-time=NUM', Numeric, "#{action} for a percentage of time") do |num|
        values << Types::PercentageOfTime.new(num)
      end
      c.on('-x expressions', '--expression=NUM', "#{action} for the given expression") do |expression|
        begin
          values << Flipper::Expression.build(JSON.parse(expression))
        rescue JSON::ParserError => e
          ui.error "JSON parse error #{e.message}"
          ui.trace(e)
          exit 1
        rescue ArgumentError => e
          ui.error "Invalid expression: #{e.message}"
          ui.trace(e)
          exit 1
        end
      end

      c.action do |feature|
        f = Flipper.feature(feature)

        if values.empty?
          f.send(action)
        else
          values.each { |value| f.send(action, value) }
        end

        ui.info feature_details(f)
      end
    end
  end

  command 'list' do |c|
    c.description = "List defined features"
    c.action do
      ui.info feature_summary(Flipper.features)
    end
  end

  command 'show' do |c|
    c.description = "Show a defined feature"
    c.action do |feature|
      ui.info feature_details(Flipper.feature(feature))
    end
  end

  command 'help' do |c|
    c.load_environment = false
    c.action do |command = nil|
      ui.info command ? @commands[command].help : help
    end
  end

  on_tail('-r path', "The path to load your application. Default: #{@require}") do |path|
    @require = path
  end

  # Options available on all commands
  on_tail('-h', '--help', 'Print help message') do
    ui.info help
    exit
  end

  # Set help documentation
  self.banner = "Usage: #{program_name} [options] <command>"
  separator ""
  separator "Commands:"

  pad = @commands.keys.map(&:length).max + 2
  @commands.each do |name, command|
    separator "  #{name.to_s.ljust(pad, " ")} #{command.description}" if command.description
  end

  separator ""
  separator "Options:"
end

Instance Attribute Details

#shellObject

Returns the value of attribute shell.



12
13
14
# File 'lib/flipper/cli.rb', line 12

def shell
  @shell
end

Class Method Details

.run(argv = ARGV) ⇒ Object



5
6
7
# File 'lib/flipper/cli.rb', line 5

def self.run(argv = ARGV)
  new.run(argv)
end

Instance Method Details

#colorize(text, colors) ⇒ Object



220
221
222
# File 'lib/flipper/cli.rb', line 220

def colorize(text, colors)
  ui.add_color(text, *colors)
end

#command(name, &block) ⇒ Object

Helper method to define a new command



138
139
140
141
# File 'lib/flipper/cli.rb', line 138

def command(name, &block)
  @commands[name] = Command.new(program_name: "#{program_name} #{name}")
  block.call(@commands[name])
end

#feature_details(feature) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/flipper/cli.rb', line 184

def feature_details(feature)
  summary = case feature.state
  when :on
    colorize("⏺ enabled", [:GREEN])
  when :off
    "⦸ disabled"
  else
    lines = feature.enabled_gates.map do |gate|
      case gate.name
      when :actor
        [ pluralize(feature.actors_value.size, 'actor', 'actors') ] +
        feature.actors_value.map { |actor| "- #{actor}" }
      when :group
        [ pluralize(feature.groups_value.size, 'group', 'groups') ] +
        feature.groups_value.map { |group| "  - #{group}" }
      when :percentage_of_actors
        "#{feature.percentage_of_actors_value}% of actors"
      when :percentage_of_time
        "#{feature.percentage_of_time_value}% of time"
      when :expression
        json = indent(JSON.pretty_generate(feature.expression_value), 2)
        "the expression: \n#{colorize(json, [:MAGENTA])}"
      end
    end

    "#{colorize("◯ conditionally enabled", [:YELLOW])} for:\n" +
    indent(lines.flatten.join("\n"), 2)
  end

  "#{colorize(feature.key, [:BOLD, :WHITE])} is #{summary}"
end

#feature_summary(features) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/flipper/cli.rb', line 153

def feature_summary(features)
  features = Array(features)
  padding = features.map { |f| f.key.to_s.length }.max

  features.map do |feature|
    summary = case feature.state
    when :on
      colorize("⏺ enabled", [:GREEN])
    when :off
      "⦸ disabled"
    else
        "#{colorize("◯ enabled", [:YELLOW])} for " + feature.enabled_gates.map do |gate|
        case gate.name
        when :actor
          pluralize feature.actors_value.size, 'actor', 'actors'
        when :group
          pluralize feature.groups_value.size, 'group', 'groups'
        when :percentage_of_actors
          "#{feature.percentage_of_actors_value}% of actors"
        when :percentage_of_time
          "#{feature.percentage_of_time_value}% of time"
        when :expression
          "an expression"
        end
      end.join(', ')
    end

    colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
  end.join("\n")
end

#indent(text, spaces) ⇒ Object



230
231
232
# File 'lib/flipper/cli.rb', line 230

def indent(text, spaces)
  text.gsub(/^/, " " * spaces)
end

#load_environment!Object



143
144
145
146
147
148
149
150
151
# File 'lib/flipper/cli.rb', line 143

def load_environment!
  ENV["FLIPPER_CLOUD_LOGGING_ENABLED"] ||= "false"
  require File.expand_path(@require)
  # Ensure all of flipper gets loaded if it hasn't already.
  require 'flipper'
rescue LoadError => e
  ui.error e.message
  exit 1
end

#pluralize(count, singular, plural) ⇒ Object



216
217
218
# File 'lib/flipper/cli.rb', line 216

def pluralize(count, singular, plural)
  "#{count} #{count == 1 ? singular : plural}"
end

#run(argv) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/flipper/cli.rb', line 118

def run(argv)
  command, *args = order(argv)

  if @commands[command]
    load_environment! if @commands[command].load_environment
    @commands[command].run(args)
  else
    ui.info help

    if command
      ui.error "Unknown command: #{command}"
      exit 1
    end
  end
rescue OptionParser::InvalidOption => e
  ui.error e.message
  exit 1
end

#uiObject



224
225
226
227
228
# File 'lib/flipper/cli.rb', line 224

def ui
  @ui ||= Bundler::UI::Shell.new.tap do |ui|
    ui.shell = shell
  end
end