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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# 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 'export' do |c|
    c.description = "Export features as JSON"
    c.action do
      export = Flipper.export(format: :json, version: 1)
      ui.info export.contents
    end
  end

  command 'cloud' do |c|
    c.description = "Flipper Cloud commands"
    c.action do |subcommand = nil, *args|
      require 'flipper/cloud/migrate'

      case subcommand
      when 'migrate'
        result = Flipper::Cloud.migrate(Flipper)
        if result.url
          ui.info "Migrating to Flipper Cloud..."
          ui.info result.url
          system("open", result.url)
        else
          message = "Migration failed (HTTP #{result.code})"
          message << ": #{result.message}" if result.message
          ui.error message
          exit 1
        end
      when 'push'
        token = args.first
        unless token
          ui.error "Usage: flipper cloud push <token>"
          exit 1
        end
        result = Flipper::Cloud.push(token, Flipper)
        if result.code == 204
          ui.info "Successfully pushed features to Flipper Cloud"
        else
          message = "Push failed (HTTP #{result.code})"
          message << ": #{result.message}" if result.message
          ui.error message
          exit 1
        end
      else
        ui.info "Usage: flipper cloud <command>"
        ui.info ""
        ui.info "Commands:"
        ui.info "  migrate  Migrate features to a new Flipper Cloud account"
        ui.info "  push     Push features to an existing Flipper Cloud project"
      end
    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



271
272
273
# File 'lib/flipper/cli.rb', line 271

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

#command(name, &block) ⇒ Object

Helper method to define a new command



189
190
191
192
# File 'lib/flipper/cli.rb', line 189

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

#feature_details(feature) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/flipper/cli.rb', line 235

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



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/flipper/cli.rb', line 204

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



281
282
283
# File 'lib/flipper/cli.rb', line 281

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

#load_environment!Object



194
195
196
197
198
199
200
201
202
# File 'lib/flipper/cli.rb', line 194

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



267
268
269
# File 'lib/flipper/cli.rb', line 267

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

#run(argv) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/flipper/cli.rb', line 169

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



275
276
277
278
279
# File 'lib/flipper/cli.rb', line 275

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