flipper-ui 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd4963986e65fb20081d23374099f2a054df7e78a81e2571ad3640831b7d0d81
4
- data.tar.gz: b94fc64dcf6752a2951c8c03bd6161c46e4850f206ebb049f4aa8b0a5b20e881
3
+ metadata.gz: 3be7bbda3ec1a09b1cc1c421766ea520a9a933eeb1628ea27281c35a174725a2
4
+ data.tar.gz: ca0375faf5a0cfd4f2b6c04435c803e9414c2e41337918ad9f8bb9f95f579e46
5
5
  SHA512:
6
- metadata.gz: c46dbab449279c506b9d5ce1d2869392b8e4f0f6b8dab59b527b64a2acf25504cb5fb3e8af2e2601176f0f7c026afdf547196d197d4d0cfad0031e8a26f270bc
7
- data.tar.gz: 95ed1aefbe02cdac6f8e938107a00a41e2e8754fa20351fa35948942557746d863d542c52b3993846828ce327b52aa4c151e1422997baea0e8099ff4c3580988
6
+ metadata.gz: effdbdd4ab7c0646dcd1a50ebd0d96fd78dc2dabb2a87ae1e524a2a4b32ce43c1b01e6e96046d1e9bc4fe298a805a2c31c713b83ad1dbfb26c31dfb70f714e90
7
+ data.tar.gz: 8d1f5b3f946a0b5d9bb42db7a497484c490cc2a2e9f15562e046ddeb41be0dba1cdda19ef1016bb5f7966ab35c600b8b57418533797b3000cec927671079ff1a
@@ -5,50 +5,13 @@
5
5
  # http://localhost:9999/
6
6
  #
7
7
  require 'bundler/setup'
8
- require "logger"
9
-
10
8
  require "flipper/ui"
11
9
  require "flipper/adapters/pstore"
12
- require "active_support/notifications"
13
10
 
14
11
  Flipper.register(:admins) { |actor|
15
12
  actor.respond_to?(:admin?) && actor.admin?
16
13
  }
17
14
 
18
- Flipper.register(:early_access) { |actor|
19
- actor.respond_to?(:early?) && actor.early?
20
- }
21
-
22
- # Setup logging of flipper calls.
23
- if ENV["LOG"] == "1"
24
- $logger = Logger.new(STDOUT)
25
- require "flipper/instrumentation/log_subscriber"
26
- Flipper::Instrumentation::LogSubscriber.logger = $logger
27
- end
28
-
29
- adapter = Flipper::Adapters::PStore.new
30
- flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
31
-
32
- Flipper::UI.configure do |config|
33
- # config.banner_text = 'Production Environment'
34
- # config.banner_class = 'danger'
35
- config.feature_creation_enabled = true
36
- config.feature_removal_enabled = true
37
- # config.show_feature_description_in_list = true
38
- config.descriptions_source = lambda do |_keys|
39
- {
40
- "search_performance_another_long_thing" => "Just to test feature name length.",
41
- "gauges_tracking" => "Should we track page views with gaug.es.",
42
- "unused" => "Not used.",
43
- "suits" => "Are suits necessary in business?",
44
- "secrets" => "Secrets are lies.",
45
- "logging" => "Log all the things.",
46
- "new_cache" => "Like the old cache but newer.",
47
- "a/b" => "Why would someone use a slash? I don't know but someone did. Let's make this really long so they regret using slashes. Please don't use slashes.",
48
- }
49
- end
50
- end
51
-
52
15
  # Example middleware to allow reading the Flipper UI but nothing else.
53
16
  class FlipperReadOnlyMiddleware
54
17
  def initialize(app)
@@ -67,18 +30,17 @@ class FlipperReadOnlyMiddleware
67
30
  end
68
31
 
69
32
  # You can uncomment these to get some default data:
70
- # flipper[:search_performance_another_long_thing].enable
71
- # flipper[:gauges_tracking].enable
72
- # flipper[:unused].disable
73
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
74
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
75
- # flipper[:secrets].enable_group :admins
76
- # flipper[:secrets].enable_group :early_access
77
- # flipper[:logging].enable_percentage_of_time 5
78
- # flipper[:new_cache].enable_percentage_of_actors 15
79
- # flipper["something/slashed"].add
80
-
81
- run Flipper::UI.app(flipper) { |builder|
33
+ # Flipper.enable(:search_performance_another_long_thing)
34
+ # Flipper.disable(:gauges_tracking)
35
+ # Flipper.disable(:unused)
36
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('1'))
37
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('6'))
38
+ # Flipper.enable_group(:secrets, :admins)
39
+ # Flipper.enable_percentage_of_time(:logging, 5)
40
+ # Flipper.enable_percentage_of_actors(:new_cache, 15)
41
+ # Flipper.add("a/b")
42
+
43
+ run Flipper::UI.app { |builder|
82
44
  builder.use Rack::Session::Cookie, secret: "_super_secret"
83
45
  builder.use FlipperReadOnlyMiddleware
84
46
  }
data/examples/ui/basic.ru CHANGED
@@ -42,16 +42,16 @@ Flipper::UI.configure do |config|
42
42
  end
43
43
 
44
44
  # You can uncomment these to get some default data:
45
- # flipper[:search_performance_another_long_thing].enable
46
- # flipper[:gauges_tracking].enable
47
- # flipper[:unused].disable
48
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
49
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
50
- # flipper[:secrets].enable_group :admins
51
- # flipper[:secrets].enable_group :early_access
52
- # flipper[:logging].enable_percentage_of_time 5
53
- # flipper[:new_cache].enable_percentage_of_actors 15
54
- # flipper["a/b"].add
45
+ # Flipper.enable(:search_performance_another_long_thing)
46
+ # Flipper.disable(:gauges_tracking)
47
+ # Flipper.disable(:unused)
48
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('1'))
49
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('6'))
50
+ # Flipper.enable_group(:secrets, :admins)
51
+ # Flipper.enable_group(:secrets, :early_access)
52
+ # Flipper.enable_percentage_of_time(:logging, 5)
53
+ # Flipper.enable_percentage_of_actors(:new_cache, 15)
54
+ # Flipper.add("a/b")
55
55
 
56
56
  run Flipper::UI.app { |builder|
57
57
  builder.use Rack::Session::Cookie, secret: "_super_secret"
data/lib/flipper/ui.rb CHANGED
@@ -39,14 +39,14 @@ module Flipper
39
39
  def self.app(flipper = nil, options = {})
40
40
  env_key = options.fetch(:env_key, 'flipper')
41
41
  rack_protection_options = options.fetch(:rack_protection, use: :authenticity_token)
42
+
42
43
  app = ->(_) { [200, { 'Content-Type' => 'text/html' }, ['']] }
43
44
  builder = Rack::Builder.new
44
45
  yield builder if block_given?
45
46
  builder.use Rack::Protection, rack_protection_options
46
47
  builder.use Rack::MethodOverride
47
48
  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
48
- builder.use Flipper::Middleware::Memoizer, env_key: env_key
49
- builder.use Flipper::UI::Middleware, env_key: env_key
49
+ builder.use Flipper::UI::Middleware, flipper: flipper, env_key: env_key
50
50
  builder.run app
51
51
  klass = self
52
52
  builder.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
@@ -151,7 +151,7 @@ module Flipper
151
151
  # location - The String location to set the Location header to.
152
152
  def redirect_to(location)
153
153
  status 302
154
- header 'Location', "#{script_name}#{location}"
154
+ header 'Location', "#{script_name}#{Rack::Utils.escape_path(location)}"
155
155
  halt [@code, @headers, ['']]
156
156
  end
157
157
 
@@ -27,7 +27,7 @@ module Flipper
27
27
  value = params['value'].to_s.strip
28
28
 
29
29
  if Util.blank?(value)
30
- error = Rack::Utils.escape("#{value.inspect} is not a valid actor value.")
30
+ error = "#{value.inspect} is not a valid actor value."
31
31
  redirect_to("/features/#{feature.key}/actors?error=#{error}")
32
32
  end
33
33
 
@@ -49,14 +49,14 @@ module Flipper
49
49
  value = params['value'].to_s.strip
50
50
 
51
51
  if Util.blank?(value)
52
- error = Rack::Utils.escape("#{value.inspect} is not a valid feature name.")
52
+ error = "#{value.inspect} is not a valid feature name."
53
53
  redirect_to("/features/new?error=#{error}")
54
54
  end
55
55
 
56
56
  feature = flipper[value]
57
57
  feature.add
58
58
 
59
- redirect_to "/features/#{Rack::Utils.escape_path(value)}"
59
+ redirect_to "/features/#{value}"
60
60
  end
61
61
  end
62
62
  end
@@ -35,7 +35,7 @@ module Flipper
35
35
 
36
36
  redirect_to("/features/#{feature.key}")
37
37
  else
38
- error = Rack::Utils.escape("The group named #{value.inspect} has not been registered.")
38
+ error = "The group named #{value.inspect} has not been registered."
39
39
  redirect_to("/features/#{feature.key}/groups?error=#{error}")
40
40
  end
41
41
  end
@@ -16,7 +16,7 @@ module Flipper
16
16
  begin
17
17
  feature.enable_percentage_of_actors params['value']
18
18
  rescue ArgumentError => exception
19
- error = Rack::Utils.escape("Invalid percentage of actors value: #{exception.message}")
19
+ error = "Invalid percentage of actors value: #{exception.message}"
20
20
  redirect_to("/features/#{@feature.key}?error=#{error}")
21
21
  end
22
22
 
@@ -16,7 +16,7 @@ module Flipper
16
16
  begin
17
17
  feature.enable_percentage_of_time params['value']
18
18
  rescue ArgumentError => exception
19
- error = Rack::Utils.escape("Invalid percentage of time value: #{exception.message}")
19
+ error = "Invalid percentage of time value: #{exception.message}"
20
20
  redirect_to("/features/#{@feature.key}?error=#{error}")
21
21
  end
22
22
 
@@ -12,6 +12,7 @@ module Flipper
12
12
  def initialize(app, options = {})
13
13
  @app = app
14
14
  @env_key = options.fetch(:env_key, 'flipper')
15
+ @flipper = options.fetch(:flipper) { Flipper }
15
16
 
16
17
  @action_collection = ActionCollection.new
17
18
 
@@ -43,7 +44,7 @@ module Flipper
43
44
  if action_class.nil?
44
45
  @app.call(env)
45
46
  else
46
- flipper = env.fetch(@env_key)
47
+ flipper = env.fetch(@env_key) { Flipper }
47
48
  action_class.run(flipper, request)
48
49
  end
49
50
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.21.0'.freeze
2
+ VERSION = '0.22.0'.freeze
3
3
  end
@@ -61,6 +61,23 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
61
61
  expect(last_response.headers['Location']).to eq('/features/search')
62
62
  end
63
63
 
64
+ context "when feature name contains space" do
65
+ before do
66
+ post 'features/sp%20ace/actors',
67
+ { 'value' => value, 'operation' => 'enable', 'authenticity_token' => token },
68
+ 'rack.session' => session
69
+ end
70
+
71
+ it 'adds item to members' do
72
+ expect(flipper["sp ace"].actors_value).to include('User;6')
73
+ end
74
+
75
+ it "redirects back to feature" do
76
+ expect(last_response.status).to be(302)
77
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
78
+ end
79
+ end
80
+
64
81
  context 'value contains whitespace' do
65
82
  let(:value) { ' User;6 ' }
66
83
 
@@ -75,7 +92,7 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
75
92
 
76
93
  it 'redirects back to feature' do
77
94
  expect(last_response.status).to be(302)
78
- expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
95
+ expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22%20is%20not%20a%20valid%20actor%20value.')
79
96
  end
80
97
  end
81
98
 
@@ -84,7 +101,7 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
84
101
 
85
102
  it 'redirects back to feature' do
86
103
  expect(last_response.status).to be(302)
87
- expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
104
+ expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22%20is%20not%20a%20valid%20actor%20value.')
88
105
  end
89
106
  end
90
107
  end
@@ -31,6 +31,24 @@ RSpec.describe Flipper::UI::Actions::BooleanGate do
31
31
  end
32
32
  end
33
33
 
34
+ context "with space in feature name" do
35
+ before do
36
+ flipper.disable :search
37
+ post 'features/sp%20ace/boolean',
38
+ { 'action' => 'Enable', 'authenticity_token' => token },
39
+ 'rack.session' => session
40
+ end
41
+
42
+ it 'updates feature' do
43
+ expect(flipper.enabled?("sp ace")).to be(true)
44
+ end
45
+
46
+ it 'redirects back to feature' do
47
+ expect(last_response.status).to be(302)
48
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
49
+ end
50
+ end
51
+
34
52
  context 'with disable' do
35
53
  before do
36
54
  flipper.enable :search
@@ -29,6 +29,24 @@ RSpec.describe Flipper::UI::Actions::Feature do
29
29
  expect(last_response.headers['Location']).to eq('/features')
30
30
  end
31
31
 
32
+ context "with space in feature name" do
33
+ before do
34
+ flipper.enable "sp ace"
35
+ delete '/features/sp%20ace',
36
+ { 'authenticity_token' => token },
37
+ 'rack.session' => session
38
+ end
39
+
40
+ it 'removes feature' do
41
+ expect(flipper.features.map(&:key)).not_to include('sp ace')
42
+ end
43
+
44
+ it 'redirects to features' do
45
+ expect(last_response.status).to be(302)
46
+ expect(last_response.headers['Location']).to eq('/features')
47
+ end
48
+ end
49
+
32
50
  context 'when feature_removal_enabled is set to false' do
33
51
  around do |example|
34
52
  begin
@@ -95,7 +95,7 @@ RSpec.describe Flipper::UI::Actions::Features do
95
95
  expect(last_response.headers['Location']).to eq('/features/notifications_next')
96
96
  end
97
97
 
98
- context 'feature name contains whitespace' do
98
+ context 'feature name has whitespace at beginning and end' do
99
99
  let(:feature_name) { ' notifications_next ' }
100
100
 
101
101
  it 'adds feature without whitespace' do
@@ -103,6 +103,19 @@ RSpec.describe Flipper::UI::Actions::Features do
103
103
  end
104
104
  end
105
105
 
106
+ context 'feature name contains space' do
107
+ let(:feature_name) { 'notifications next' }
108
+
109
+ it 'adds feature with space' do
110
+ expect(flipper.features.map(&:key)).to include('notifications next')
111
+ end
112
+
113
+ it 'redirects to feature' do
114
+ expect(last_response.status).to be(302)
115
+ expect(last_response.headers['Location']).to eq('/features/notifications%20next')
116
+ end
117
+ end
118
+
106
119
  context 'for an invalid feature name' do
107
120
  context 'empty feature name' do
108
121
  let(:feature_name) { '' }
@@ -113,7 +126,7 @@ RSpec.describe Flipper::UI::Actions::Features do
113
126
 
114
127
  it 'redirects back to feature' do
115
128
  expect(last_response.status).to be(302)
116
- expect(last_response.headers['Location']).to eq('/features/new?error=%22%22+is+not+a+valid+feature+name.')
129
+ expect(last_response.headers['Location']).to eq('/features/new?error=%22%22%20is%20not%20a%20valid%20feature%20name.')
117
130
  end
118
131
  end
119
132
 
@@ -126,7 +139,7 @@ RSpec.describe Flipper::UI::Actions::Features do
126
139
 
127
140
  it 'redirects back to feature' do
128
141
  expect(last_response.status).to be(302)
129
- expect(last_response.headers['Location']).to eq('/features/new?error=%22%22+is+not+a+valid+feature+name.')
142
+ expect(last_response.headers['Location']).to eq('/features/new?error=%22%22%20is%20not%20a%20valid%20feature%20name.')
130
143
  end
131
144
  end
132
145
  end
@@ -60,6 +60,23 @@ RSpec.describe Flipper::UI::Actions::GroupsGate do
60
60
  expect(last_response.headers['Location']).to eq('/features/search')
61
61
  end
62
62
 
63
+ context 'feature name contains space' do
64
+ before do
65
+ post 'features/sp%20ace/groups',
66
+ { 'value' => group_name, 'operation' => 'enable', 'authenticity_token' => token },
67
+ 'rack.session' => session
68
+ end
69
+
70
+ it 'adds item to members' do
71
+ expect(flipper["sp ace"].groups_value).to include('admins')
72
+ end
73
+
74
+ it 'redirects back to feature' do
75
+ expect(last_response.status).to be(302)
76
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
77
+ end
78
+ end
79
+
63
80
  context 'group name contains whitespace' do
64
81
  let(:group_name) { ' admins ' }
65
82
 
@@ -74,7 +91,7 @@ RSpec.describe Flipper::UI::Actions::GroupsGate do
74
91
 
75
92
  it 'redirects back to feature' do
76
93
  expect(last_response.status).to be(302)
77
- expect(last_response.headers['Location']).to eq('/features/search/groups?error=The+group+named+%22not_here%22+has+not+been+registered.')
94
+ expect(last_response.headers['Location']).to eq('/features/search/groups?error=The%20group%20named%20%22not_here%22%20has%20not%20been%20registered.')
78
95
  end
79
96
  end
80
97
 
@@ -83,7 +100,7 @@ RSpec.describe Flipper::UI::Actions::GroupsGate do
83
100
 
84
101
  it 'redirects back to feature' do
85
102
  expect(last_response.status).to be(302)
86
- expect(last_response.headers['Location']).to eq('/features/search/groups?error=The+group+named+%22%22+has+not+been+registered.')
103
+ expect(last_response.headers['Location']).to eq('/features/search/groups?error=The%20group%20named%20%22%22%20has%20not%20been%20registered.')
87
104
  end
88
105
  end
89
106
 
@@ -92,7 +109,7 @@ RSpec.describe Flipper::UI::Actions::GroupsGate do
92
109
 
93
110
  it 'redirects back to feature' do
94
111
  expect(last_response.status).to be(302)
95
- expect(last_response.headers['Location']).to eq('/features/search/groups?error=The+group+named+%22%22+has+not+been+registered.')
112
+ expect(last_response.headers['Location']).to eq('/features/search/groups?error=The%20group%20named%20%22%22%20has%20not%20been%20registered.')
96
113
  end
97
114
  end
98
115
  end
@@ -30,6 +30,23 @@ RSpec.describe Flipper::UI::Actions::PercentageOfActorsGate do
30
30
  end
31
31
  end
32
32
 
33
+ context 'with space in feature name' do
34
+ before do
35
+ post 'features/sp%20ace/percentage_of_actors',
36
+ { 'value' => '24', 'authenticity_token' => token },
37
+ 'rack.session' => session
38
+ end
39
+
40
+ it 'enables the feature' do
41
+ expect(flipper["sp ace"].percentage_of_actors_value).to be(24)
42
+ end
43
+
44
+ it 'redirects back to feature' do
45
+ expect(last_response.status).to be(302)
46
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
47
+ end
48
+ end
49
+
33
50
  context 'with invalid value' do
34
51
  before do
35
52
  post 'features/search/percentage_of_actors',
@@ -43,7 +60,7 @@ RSpec.describe Flipper::UI::Actions::PercentageOfActorsGate do
43
60
 
44
61
  it 'redirects back to feature' do
45
62
  expect(last_response.status).to be(302)
46
- expect(last_response.headers['Location']).to eq('/features/search?error=Invalid+percentage+of+actors+value%3A+value+must+be+a+positive+number+less+than+or+equal+to+100%2C+but+was+555')
63
+ expect(last_response.headers['Location']).to eq('/features/search?error=Invalid%20percentage%20of%20actors%20value:%20value%20must%20be%20a%20positive%20number%20less%20than%20or%20equal%20to%20100,%20but%20was%20555')
47
64
  end
48
65
  end
49
66
  end
@@ -30,6 +30,23 @@ RSpec.describe Flipper::UI::Actions::PercentageOfTimeGate do
30
30
  end
31
31
  end
32
32
 
33
+ context 'with space in feature name' do
34
+ before do
35
+ post 'features/sp%20ace/percentage_of_time',
36
+ { 'value' => '24', 'authenticity_token' => token },
37
+ 'rack.session' => session
38
+ end
39
+
40
+ it 'enables the feature' do
41
+ expect(flipper["sp ace"].percentage_of_time_value).to be(24)
42
+ end
43
+
44
+ it 'redirects back to feature' do
45
+ expect(last_response.status).to be(302)
46
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
47
+ end
48
+ end
49
+
33
50
  context 'with invalid value' do
34
51
  before do
35
52
  post 'features/search/percentage_of_time',
@@ -43,7 +60,7 @@ RSpec.describe Flipper::UI::Actions::PercentageOfTimeGate do
43
60
 
44
61
  it 'redirects back to feature' do
45
62
  expect(last_response.status).to be(302)
46
- expect(last_response.headers['Location']).to eq('/features/search?error=Invalid+percentage+of+time+value%3A+value+must+be+a+positive+number+less+than+or+equal+to+100%2C+but+was+555')
63
+ expect(last_response.headers['Location']).to eq('/features/search?error=Invalid%20percentage%20of%20time%20value:%20value%20must%20be%20a%20positive%20number%20less%20than%20or%20equal%20to%20100,%20but%20was%20555')
47
64
  end
48
65
  end
49
66
  end
@@ -24,19 +24,6 @@ RSpec.describe Flipper::UI do
24
24
  end
25
25
  end
26
26
 
27
- describe 'Initializing middleware lazily with a block' do
28
- let(:app) do
29
- build_app(-> { flipper })
30
- end
31
-
32
- it 'works' do
33
- flipper.enable :some_great_feature
34
- get '/features'
35
- expect(last_response.status).to be(200)
36
- expect(last_response.body).to include('some_great_feature')
37
- end
38
- end
39
-
40
27
  describe 'Request method unsupported by action' do
41
28
  it 'raises error' do
42
29
  expect do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-09 00:00:00.000000000 Z
11
+ date: 2021-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -56,14 +56,14 @@ dependencies:
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: 0.21.0
59
+ version: 0.22.0
60
60
  type: :runtime
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: 0.21.0
66
+ version: 0.22.0
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: erubi
69
69
  requirement: !ruby/object:Gem::Requirement