blackbeard 0.0.3.1 → 0.0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +31 -20
- data/dashboard/public/stylesheets/application.css +4 -0
- data/dashboard/routes/features.rb +31 -0
- data/dashboard/routes/groups.rb +2 -2
- data/dashboard/routes/metrics.rb +6 -6
- data/dashboard/routes/tests.rb +2 -2
- data/dashboard/views/features/index.erb +21 -0
- data/dashboard/views/features/show.erb +163 -0
- data/dashboard/views/groups/show.erb +1 -1
- data/dashboard/views/layout.erb +2 -1
- data/lib/blackbeard.rb +18 -7
- data/lib/blackbeard/configuration.rb +3 -2
- data/lib/blackbeard/context.rb +11 -7
- data/lib/blackbeard/dashboard.rb +2 -0
- data/lib/blackbeard/errors.rb +4 -0
- data/lib/blackbeard/feature.rb +45 -0
- data/lib/blackbeard/feature_rollout.rb +47 -0
- data/lib/blackbeard/metric.rb +22 -4
- data/lib/blackbeard/pirate.rb +11 -5
- data/lib/blackbeard/redis_store.rb +4 -0
- data/lib/blackbeard/storable.rb +72 -6
- data/lib/blackbeard/storable_attributes.rb +50 -3
- data/lib/blackbeard/storable_has_many.rb +1 -0
- data/lib/blackbeard/storable_has_set.rb +1 -0
- data/lib/blackbeard/version.rb +1 -1
- data/spec/blackbeard_spec.rb +13 -0
- data/spec/configuration_spec.rb +6 -0
- data/spec/context_spec.rb +12 -12
- data/spec/dashboard/features_spec.rb +52 -0
- data/spec/dashboard/groups_spec.rb +4 -4
- data/spec/dashboard/metrics_spec.rb +6 -6
- data/spec/dashboard/tests_spec.rb +4 -4
- data/spec/feature_rollout_spec.rb +147 -0
- data/spec/feature_spec.rb +58 -0
- data/spec/group_spec.rb +1 -1
- data/spec/metric_data/total_spec.rb +1 -1
- data/spec/metric_data/unique_spec.rb +1 -1
- data/spec/metric_spec.rb +5 -5
- data/spec/pirate_spec.rb +16 -12
- data/spec/redis_store_spec.rb +6 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/storable_attributes_spec.rb +58 -6
- data/spec/storable_has_many_spec.rb +2 -2
- data/spec/storable_has_set_spec.rb +1 -1
- data/spec/storable_spec.rb +119 -23
- data/spec/test_spec.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a8e907aad46889da2e0abfab0cd85fdc87c93a4e
|
4
|
+
data.tar.gz: 125429d019e4e2e4767f18e2428faac8db1bff8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94047af0590aa7109951b25870f1c46f1b12d9231c09af49f141c025c03dcca9468a1b48d75878e1154ed867cc9dc768175011af0d9d42725251757976478527
|
7
|
+
data.tar.gz: 7c407ffcffb34c88a29552e85a44a488bf377a7b21bc3f89f3d8d5c7213e3b888fcc5dad59ad8e1293e07a88b088694a824970c8f74a004eb82f5d2660c7631b
|
data/README.md
CHANGED
@@ -135,20 +135,20 @@ $pirate.add_total(:revenue, +119.95) # can also accept floats
|
|
135
135
|
|
136
136
|
### Chaining Metrics
|
137
137
|
|
138
|
-
Context methods that
|
138
|
+
Context methods that increment metrics always return the context, so you can chain them together.
|
139
139
|
|
140
140
|
```ruby
|
141
141
|
$pirate.set_context(user)
|
142
142
|
$pirate.add_total(:like, +1).add_unique(:likers)
|
143
|
-
|
144
143
|
$pirate.context(user).add_total(:like, +1).add_unique(:likers)
|
145
144
|
```
|
146
145
|
|
147
146
|
|
148
|
-
### Defining Tests
|
147
|
+
### Defining AB Tests
|
149
148
|
|
150
|
-
|
149
|
+
AB tests are defined in your views, controller or anywhere in your app via the global $pirate. There is no configuration necessary (but see the gotcha below).
|
151
150
|
|
151
|
+
**_Note that ab_tests are implmented, but experiments to control them is not. AB tests and experiments are in active development._**
|
152
152
|
|
153
153
|
In a view:
|
154
154
|
|
@@ -162,7 +162,7 @@ In a controller:
|
|
162
162
|
@onboarding_path = $pirate.ab_test(:new_onboarding, :control => '/onboarding', :welcome_flow => '/welcome') %>
|
163
163
|
```
|
164
164
|
|
165
|
-
You can call the
|
165
|
+
You can call the test multiple times with different variations:
|
166
166
|
|
167
167
|
```ruby
|
168
168
|
@button_bg_color = $pirate.ab_test(:button_color, :control => "#FFF", :black => "#000")
|
@@ -183,13 +183,7 @@ if $pirate.ab_test(:join_form) == :long_version do
|
|
183
183
|
end
|
184
184
|
```
|
185
185
|
|
186
|
-
If you're
|
187
|
-
|
188
|
-
```ruby
|
189
|
-
if $pirate.active?(:friend_feed){ ... } # shorthand for test(:friend_feed) == :active
|
190
|
-
```
|
191
|
-
|
192
|
-
GOTCHA #1: It's good to avoid elsif and case statements when testing for features. Blackbeard learns about the features and their variations dynamically. If you're not passing in your variations as a hash, but only using conditionals, you can ensure all your variations are available with:
|
186
|
+
GOTCHA #1: It's good to avoid elsif and case statements. Blackbeard learns about the tests and their variations dynamically. If you're not passing in your variations as a hash, but only using conditionals, you can ensure all your variations are available with:
|
193
187
|
|
194
188
|
```ruby
|
195
189
|
$pirate.test(:friend_feed).add_variations(:variation_one, :variation_two, ...)
|
@@ -197,14 +191,26 @@ $pirate.test(:friend_feed).add_variations(:variation_one, :variation_two, ...)
|
|
197
191
|
|
198
192
|
Look at the dashboard to see which variations are registered.
|
199
193
|
|
200
|
-
|
194
|
+
Test do not turn on automatically. When you first deploy the test will be set to `:inactive` or `:control`, `:off`, or `:default` if any of those variations are defined.
|
201
195
|
|
202
196
|
GOTCHA #2: If you do not define the :inactive, :control, :off or :default variations, the result will be nil. This is the desired behavior but it may be confusing.
|
203
197
|
|
204
198
|
```ruby
|
205
199
|
$pirate.ab_test(:new_onboarding, :one => 'one', :two => 'two') # is the same as the next line
|
206
|
-
$pirate.ab_test(:new_onboarding, :inactive => nil, :one => 'one', :two => 'two') # nil when
|
207
|
-
$pirate.ab_test(:new_onboarding, :default => 'one', :two => 'two') # => 'one' when
|
200
|
+
$pirate.ab_test(:new_onboarding, :inactive => nil, :one => 'one', :two => 'two') # nil when test is inactive
|
201
|
+
$pirate.ab_test(:new_onboarding, :default => 'one', :two => 'two') # => 'one' when test is inactive
|
202
|
+
```
|
203
|
+
|
204
|
+
### Features
|
205
|
+
|
206
|
+
Features are very similar to AB Tests but they are only active or inactive. You can control how they are rolled out to groups and even individuals in the dashboard.
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
# With the context set
|
210
|
+
if $pirate.feature_active?(:friend_feed){ ... }
|
211
|
+
|
212
|
+
# Or outside a web request
|
213
|
+
if $pirate.context(user).feature_active?(:friend_feed){ ... }
|
208
214
|
```
|
209
215
|
|
210
216
|
### Defining groups
|
@@ -214,10 +220,6 @@ $pirate.define_group(:admin) do |user, context|
|
|
214
220
|
user.admin? # true, false
|
215
221
|
end
|
216
222
|
|
217
|
-
$pirate.define_group(:medalist) do |user, context|
|
218
|
-
user.engagement_level # nil, :bronze, :silver, :gold
|
219
|
-
end
|
220
|
-
|
221
223
|
$pirate.define_group(:seo_traffic) do |user, context|
|
222
224
|
context.session.refer =~ /google.com/ # remember to store refer in sessions
|
223
225
|
end
|
@@ -231,6 +233,16 @@ $pirate.define_group(:purchasers) do |user, context|
|
|
231
233
|
end
|
232
234
|
```
|
233
235
|
|
236
|
+
If your group is segments, include a list possible segments.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
$pirate.define_group(:medalist, [:bronze, :silver, :gold]) do |user, context|
|
240
|
+
user.engagement_level # nil, :bronze, :silver, :gold
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
If your group definition block returns an uninitialized segment, it wil be initialized automatically.
|
245
|
+
|
234
246
|
## Contributing
|
235
247
|
|
236
248
|
1. Fork it
|
@@ -238,4 +250,3 @@ end
|
|
238
250
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
239
251
|
4. Push to the branch (`git push origin my-new-feature`)
|
240
252
|
5. Create new Pull Request
|
241
|
-
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Blackbeard
|
2
|
+
module DashboardRoutes
|
3
|
+
class Features < Base
|
4
|
+
|
5
|
+
get '/features' do
|
6
|
+
@features = Feature.all
|
7
|
+
erb 'features/index'.to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
get "/features/:id" do
|
11
|
+
@feature = Feature.find(params[:id]) or pass
|
12
|
+
@groups = Group.all
|
13
|
+
erb 'features/show'.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
post "/features/:id" do
|
17
|
+
@feature = Feature.find(params[:id]) or pass
|
18
|
+
@feature.update_attributes(params)
|
19
|
+
"OK"
|
20
|
+
end
|
21
|
+
|
22
|
+
post "/features/:id/groups/:group_id" do
|
23
|
+
@feature = Feature.find(params[:id]) or pass
|
24
|
+
@feature.set_segments_for(params[:group_id], params[:segments])
|
25
|
+
@feature.save
|
26
|
+
"OK"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/dashboard/routes/groups.rb
CHANGED
@@ -7,12 +7,12 @@ module Blackbeard
|
|
7
7
|
end
|
8
8
|
|
9
9
|
get '/groups/:id' do
|
10
|
-
@group = Group.
|
10
|
+
@group = Group.find(params[:id]) or pass
|
11
11
|
erb 'groups/show'.to_sym
|
12
12
|
end
|
13
13
|
|
14
14
|
post "/groups/:id" do
|
15
|
-
@group = Group.
|
15
|
+
@group = Group.find(params[:id]) or pass
|
16
16
|
@group.update_attributes(params)
|
17
17
|
"OK"
|
18
18
|
end
|
data/dashboard/routes/metrics.rb
CHANGED
@@ -7,21 +7,21 @@ module Blackbeard
|
|
7
7
|
end
|
8
8
|
|
9
9
|
get "/metrics/:type/:type_id" do
|
10
|
-
@metric = Metric.
|
11
|
-
@group = Group.
|
10
|
+
@metric = Metric.find(params[:type], params[:type_id]) or pass
|
11
|
+
@group = Group.find(params[:group_id]) if params[:group_id]
|
12
12
|
erb 'metrics/show'.to_sym
|
13
13
|
end
|
14
14
|
|
15
15
|
post "/metrics/:type/:type_id" do
|
16
|
-
@metric = Metric.
|
16
|
+
@metric = Metric.find(params[:type], params[:type_id]) or pass
|
17
17
|
@metric.update_attributes(params)
|
18
18
|
"OK"
|
19
19
|
end
|
20
20
|
|
21
21
|
post "/metrics/:type/:type_id/groups" do
|
22
|
-
@metric = Metric.
|
23
|
-
@group = Group.
|
24
|
-
@metric.add_group(@group)
|
22
|
+
@metric = Metric.find(params[:type], params[:type_id]) or pass
|
23
|
+
@group = Group.find(params[:group_id])
|
24
|
+
@metric.add_group(@group) if @group
|
25
25
|
redirect url("metrics/#{@metric.type}/#{@metric.type_id}?group_id=#{@group.id}")
|
26
26
|
end
|
27
27
|
|
data/dashboard/routes/tests.rb
CHANGED
@@ -8,12 +8,12 @@ module Blackbeard
|
|
8
8
|
end
|
9
9
|
|
10
10
|
get "/tests/:id" do
|
11
|
-
@test = Test.
|
11
|
+
@test = Test.find(params[:id]) or pass
|
12
12
|
erb 'tests/show'.to_sym
|
13
13
|
end
|
14
14
|
|
15
15
|
post "/tests/:id" do
|
16
|
-
@test = Test.
|
16
|
+
@test = Test.find(params[:id]) or pass
|
17
17
|
@test.update_attributes(params)
|
18
18
|
"OK"
|
19
19
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1>Features</h1>
|
3
|
+
<p>Features are large pieces of functionality where you want to control their availability either for the purpose of testing or rolling out.</p>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<% if @features.any? %>
|
7
|
+
<div class="panel panel-primary">
|
8
|
+
<div class="panel-heading">
|
9
|
+
<h3 class="panel-title">Known Features</h3>
|
10
|
+
</div>
|
11
|
+
<ul class="list-group">
|
12
|
+
<% @features.each do |feature| %>
|
13
|
+
<li class="list-group-item"><a href="<%= url("features/#{feature.id}") %>"><%= feature.name %></a></li>
|
14
|
+
<% end %>
|
15
|
+
</ul>
|
16
|
+
</div>
|
17
|
+
<% else %>
|
18
|
+
<div class="alert alert-danger">
|
19
|
+
<strong>No features!</strong> Something may be wrong or you may just need to define some features.
|
20
|
+
</div>
|
21
|
+
<% end %>
|
@@ -0,0 +1,163 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<div id="status-buttons-group" class="btn-group pull-right" data-current-status="<%= @feature.status %>">
|
3
|
+
<button type="button" class="btn" value="active" data-selected-class="btn-success">Active</button>
|
4
|
+
<button type="button" class="btn" value="rollout" data-selected-class="btn-warning">Roll Out</button>
|
5
|
+
<button type="button" class="btn" value="inactive" data-selected-class="btn-danger">Inactive</button>
|
6
|
+
</div>
|
7
|
+
<h1><span id="editable-name"><%= @feature.name %></span> <small>Feature</small></h1>
|
8
|
+
<p id="editable-description"><%= @feature.description %></p>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div id="active" class="feature-status-panel hidden">
|
12
|
+
<p>This feature is <em>active</em> for all users.</p>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div id="inactive" class="feature-status-panel hidden">
|
16
|
+
<p>This feature is <em>inactive</em> for all users.</p>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<div id="rollout" class="feature-status-panel hidden">
|
20
|
+
<div class="panel panel-primary">
|
21
|
+
<div class="panel-heading">
|
22
|
+
<h3 class="panel-title">Rollout Options</h3>
|
23
|
+
</div>
|
24
|
+
<div class="panel-body">
|
25
|
+
<p>Activate feature for all visitors:</p>
|
26
|
+
<div class="progress" id="editable-visitors-rate">
|
27
|
+
<div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%; overflow: visible;">
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
<p>Activate feature for logged in users:</p>
|
31
|
+
<div class="progress" id="editable-users-rate">
|
32
|
+
<div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%; overflow: visible;">
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
<p>Activate feature for group segments:</p>
|
36
|
+
<table class="table">
|
37
|
+
<thead>
|
38
|
+
<tr>
|
39
|
+
<th>Group</th>
|
40
|
+
<th>Active Segments</th>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<% @groups.each do |group| %>
|
44
|
+
<tr>
|
45
|
+
<td><%= group.name %></td>
|
46
|
+
<td><a href="#" id="editable-group-segments-<%= group.id %>"></a></td>
|
47
|
+
</tr>
|
48
|
+
<% end if @groups.any? %>
|
49
|
+
</table>
|
50
|
+
</div>
|
51
|
+
<div class="panel-footer">Note: 100% of visitors is the same as active. 50% of visitors & 100% of logged in users will be more than 50% of all traffic.</div>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
|
55
|
+
<!--Load the AJAX API-->
|
56
|
+
<script src="<%= url('javascripts/jquery-1.10.2.min.js') %>"></script>
|
57
|
+
<script src="<%= url('javascripts/bootstrap.min.js') %>"></script>
|
58
|
+
<script src="<%= url('bootstrap3-editable/js/bootstrap-editable.min.js') %>"></script>
|
59
|
+
<script type="text/javascript">
|
60
|
+
//turn to inline mode
|
61
|
+
$.fn.editable.defaults.send = 'always';
|
62
|
+
$.fn.editable.defaults.params = function(params) {
|
63
|
+
var obj = {};
|
64
|
+
obj[params.name] = params.value;
|
65
|
+
return obj;
|
66
|
+
}
|
67
|
+
|
68
|
+
$("#status-buttons-group").on("click", ":button", function (e) {
|
69
|
+
e.preventDefault();
|
70
|
+
var status = $(this).val();
|
71
|
+
$('#status-buttons-group').data("current-status", status);
|
72
|
+
$.post( "<%= url("features/#{@feature.id}") %>", { status: status } , updateButtonStates);
|
73
|
+
});
|
74
|
+
|
75
|
+
function updateButtonStates(){
|
76
|
+
var v = $('#status-buttons-group').data("current-status");
|
77
|
+
$( "#status-buttons-group :button" ).removeClass();
|
78
|
+
$( "#status-buttons-group :button" ).addClass("btn");
|
79
|
+
var selectedClass = $( "#status-buttons-group :button[value='"+v+"']" ).data("selected-class");
|
80
|
+
$( "#status-buttons-group :button[value='"+v+"']" ).addClass( selectedClass );
|
81
|
+
$( ".feature-status-panel" ).addClass("hidden");
|
82
|
+
$( "#"+v ).removeClass("hidden");
|
83
|
+
}
|
84
|
+
|
85
|
+
$(document).ready(updateButtonStates);
|
86
|
+
|
87
|
+
$(document).ready(function() {
|
88
|
+
<% @groups.each do |group| %>
|
89
|
+
$('#editable-group-segments-<%= group.id %>').editable({
|
90
|
+
type: 'checklist',
|
91
|
+
emptytext: 'No segments active',
|
92
|
+
title: "Select segments",
|
93
|
+
url: '<%= url("features/#{@feature.id}/groups/#{group.id}")%>',
|
94
|
+
name: 'segments',
|
95
|
+
value: [<%= @feature.segments_for(group.id).map{|s| "'#{s}'"}.join(',') %>],
|
96
|
+
source: [<%= group.segments.map{|s| "{value: '#{s}', text: '#{s}'}"}.join(',') %>]
|
97
|
+
});
|
98
|
+
<% end %>
|
99
|
+
|
100
|
+
$('#editable-users-rate').editable({
|
101
|
+
type: 'number',
|
102
|
+
value: <%= @feature.users_rate %>,
|
103
|
+
display: function(value, sourceData) {
|
104
|
+
var color = "#fff";
|
105
|
+
if (value < 10) {
|
106
|
+
color = "#000";
|
107
|
+
}
|
108
|
+
$(".progress-bar", this).attr("aria-valuenow", value).html(value + "% of users").width(value+'%').css('color', color);
|
109
|
+
},
|
110
|
+
url: '<%= url("features/#{@feature.id}") %>',
|
111
|
+
name: 'users_rate',
|
112
|
+
title: '% of logged in users',
|
113
|
+
validate: function(value) {
|
114
|
+
if($.trim(value) == '') {
|
115
|
+
return 'This field is required';
|
116
|
+
}
|
117
|
+
}
|
118
|
+
});
|
119
|
+
|
120
|
+
|
121
|
+
$('#editable-visitors-rate').editable({
|
122
|
+
type: 'number',
|
123
|
+
value: <%= @feature.visitors_rate %>,
|
124
|
+
display: function(value, sourceData) {
|
125
|
+
var color = "#fff";
|
126
|
+
if (value < 10) {
|
127
|
+
color = "#000";
|
128
|
+
}
|
129
|
+
$(".progress-bar", this).attr("aria-valuenow", value).html(value + "% of visitors").width(value+'%').css('color', color);
|
130
|
+
},
|
131
|
+
url: '<%= url("features/#{@feature.id}") %>',
|
132
|
+
name: 'visitors_rate',
|
133
|
+
title: '% of all visitors',
|
134
|
+
validate: function(value) {
|
135
|
+
if($.trim(value) == '') {
|
136
|
+
return 'This field is required';
|
137
|
+
}
|
138
|
+
}
|
139
|
+
});
|
140
|
+
|
141
|
+
|
142
|
+
$('#editable-name').editable({
|
143
|
+
placement: 'right',
|
144
|
+
type: 'text',
|
145
|
+
url: '<%= url("features/#{@feature.id}") %>',
|
146
|
+
name: 'name',
|
147
|
+
title: 'Edit feature name',
|
148
|
+
validate: function(value) {
|
149
|
+
if($.trim(value) == '') {
|
150
|
+
return 'This field is required';
|
151
|
+
}
|
152
|
+
}
|
153
|
+
});
|
154
|
+
$('#editable-description').editable({
|
155
|
+
placement: 'bottom',
|
156
|
+
emptytext: 'Give this feature a description',
|
157
|
+
type: 'textarea',
|
158
|
+
url: '<%= url("features/#{@feature.id}") %>',
|
159
|
+
name: 'description',
|
160
|
+
title: 'Edit feature description'
|
161
|
+
});
|
162
|
+
});
|
163
|
+
</script>
|
data/dashboard/views/layout.erb
CHANGED
@@ -33,9 +33,10 @@
|
|
33
33
|
</div>
|
34
34
|
<div class="navbar-collapse collapse">
|
35
35
|
<ul class="nav navbar-nav">
|
36
|
+
<li><a href="<%= url('features') %>">Features</a></li>
|
36
37
|
<li><a href="<%= url('groups') %>">Groups</a></li>
|
37
38
|
<li><a href="<%= url('metrics') %>">Metrics</a></li>
|
38
|
-
|
39
|
+
<!-- li><a href="<%= url('tests') %>">Tests</a></li -->
|
39
40
|
</ul>
|
40
41
|
</div><!--/.nav-collapse -->
|
41
42
|
</div>
|
data/lib/blackbeard.rb
CHANGED
@@ -2,15 +2,26 @@ require "blackbeard/configuration"
|
|
2
2
|
require "blackbeard/pirate"
|
3
3
|
|
4
4
|
module Blackbeard
|
5
|
-
|
6
|
-
attr_accessor :config
|
5
|
+
class << self
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
def configure!
|
8
|
+
@config = Configuration.new
|
9
|
+
yield config
|
10
|
+
end
|
11
|
+
|
12
|
+
def config
|
13
|
+
@config ||= Configuration.new
|
14
|
+
end
|
13
15
|
|
16
|
+
def configure
|
17
|
+
yield config
|
18
|
+
end
|
19
|
+
|
20
|
+
def pirate
|
21
|
+
yield(config) if block_given?
|
22
|
+
Blackbeard::Pirate.new
|
23
|
+
end
|
24
|
+
end
|
14
25
|
end
|
15
26
|
|
16
27
|
Blackbeard.pirate {}
|