blackbeard 0.0.3.1 → 0.0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 {}
|