spinach 0.1.4 → 0.1.5

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.
Files changed (53) hide show
  1. data/.gitignore +1 -0
  2. data/.yardopts +6 -0
  3. data/Gemfile +1 -0
  4. data/Guardfile +7 -0
  5. data/Rakefile +1 -0
  6. data/Readme.md +147 -10
  7. data/features/{generate_features.feature → automatic_feature_generation.feature} +0 -0
  8. data/features/steps/automatic_feature_generation.rb +5 -2
  9. data/features/steps/exit_status.rb +8 -3
  10. data/features/steps/feature_name_guessing.rb +4 -1
  11. data/features/steps/reporting/display_run_summary.rb +7 -4
  12. data/features/steps/reporting/error_reporting.rb +7 -2
  13. data/features/steps/reporting/show_step_source_location.rb +9 -5
  14. data/features/steps/reporting/undefined_feature_reporting.rb +4 -1
  15. data/features/steps/rspec_compatibility.rb +6 -2
  16. data/features/support/spinach_runner.rb +2 -9
  17. data/lib/spinach/capybara.rb +3 -4
  18. data/lib/spinach/cli.rb +0 -1
  19. data/lib/spinach/config.rb +0 -8
  20. data/lib/spinach/dsl.rb +4 -13
  21. data/lib/spinach/exceptions.rb +1 -1
  22. data/lib/spinach/feature_steps.rb +1 -9
  23. data/lib/spinach/generators/feature_generator.rb +3 -2
  24. data/lib/spinach/generators.rb +1 -1
  25. data/lib/spinach/hookable.rb +81 -0
  26. data/lib/spinach/hooks.rb +132 -0
  27. data/lib/spinach/reporter/stdout/error_reporting.rb +163 -0
  28. data/lib/spinach/reporter/stdout.rb +3 -151
  29. data/lib/spinach/reporter.rb +39 -25
  30. data/lib/spinach/runner/{feature.rb → feature_runner.rb} +5 -15
  31. data/lib/spinach/runner/scenario_runner.rb +65 -0
  32. data/lib/spinach/runner.rb +5 -11
  33. data/lib/spinach/version.rb +1 -1
  34. data/lib/spinach.rb +20 -8
  35. data/spinach.gemspec +2 -3
  36. data/test/spinach/capybara_test.rb +4 -3
  37. data/test/spinach/cli_test.rb +0 -1
  38. data/test/spinach/feature_steps_test.rb +6 -23
  39. data/test/spinach/generators/feature_generator_test.rb +2 -2
  40. data/test/spinach/generators_test.rb +2 -2
  41. data/test/spinach/hookable_test.rb +59 -0
  42. data/test/spinach/hooks_test.rb +28 -0
  43. data/test/spinach/reporter/stdout/error_reporting_test.rb +265 -0
  44. data/test/spinach/reporter/stdout_test.rb +1 -238
  45. data/test/spinach/reporter_test.rb +58 -103
  46. data/test/spinach/runner/{feature_test.rb → feature_runner_test.rb} +21 -23
  47. data/test/spinach/runner/scenario_runner_test.rb +111 -0
  48. data/test/spinach/runner_test.rb +1 -1
  49. data/test/spinach_test.rb +19 -18
  50. data/test/test_helper.rb +1 -1
  51. metadata +60 -61
  52. data/lib/spinach/runner/scenario.rb +0 -77
  53. data/test/spinach/runner/scenario_test.rb +0 -120
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ .rbx
3
4
  .bundle
4
5
  .config
5
6
  .yardoc
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --title "Spinach"
2
+ --no-private
3
+ -m markdown
4
+ -r Readme.md
5
+ lib/**/*.rb
6
+ Readme.md
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
  group :test do
7
7
  gem 'guard'
8
8
  gem 'guard-minitest'
9
+ gem 'guard-spinach'
9
10
  end
10
11
 
11
12
  group :darwin do
data/Guardfile CHANGED
@@ -3,3 +3,10 @@ guard 'minitest' do
3
3
  watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
4
4
  watch(%r|^test/test_helper\.rb|) { "test" }
5
5
  end
6
+
7
+ guard 'spinach' do
8
+ watch(%r|^features/(.*)\.feature|)
9
+ watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m|
10
+ "features/#{m[1]}#{m[2]}.feature"
11
+ end
12
+ end
data/Rakefile CHANGED
@@ -9,6 +9,7 @@ Rake::TestTask.new do |t|
9
9
  # t.loader = :direct
10
10
  end
11
11
 
12
+ desc 'Run spinach features'
12
13
  task :spinach do
13
14
  exec "bundle exec spinach"
14
15
  end
data/Readme.md CHANGED
@@ -1,15 +1,152 @@
1
- # About Spinach
2
- Spinach is a BDD framework on top of gherkin
1
+ # Spinach - BDD framework on top of Gherkin [![Build Status](https://secure.travis-ci.org/codegram/spinach.png)](http://travis-ci.org/codegram/spinach)
3
2
 
4
- ![](http://farm1.static.flickr.com/58/200481513_a1a0aa265a.jpg)
3
+ Spinach is a high-level BDD framework that leverages the expressive
4
+ [Gherkin language][gherkin] (used by [Cucumber][cucumber]) to help you define
5
+ executable specifications of your application or library's acceptance criteria.
5
6
 
6
- # Documentation
7
- [Spinach documentation at rubydoc.info](http://rubydoc.info/github/codegram/spinach/master/frames)
7
+ Conceived as an alternative to Cucumber, here are some of its design goals:
8
8
 
9
- # Testimonials
10
- ![](http://www.80stees.com/images/products/Popeye_the_Sailor_Man_I_Popeye_Spinach-T-link.jpg)
9
+ * Step maintanability: since features map to their own classes, their steps are
10
+ just methods of that class. This encourages step encapsulation.
11
11
 
12
- *Popeye the Sailor*
12
+ * Step reusability: In case you want to reuse steps across features, you can
13
+ always wrap those in plain ol' Ruby modules.
13
14
 
14
- # Build status
15
- [![Build Status](https://secure.travis-ci.org/codegram/spinach.png)](http://travis-ci.org/codegram/spinach)
15
+ Spinach is tested against MRI 1.9.2, 1.9.3. Rubinius 2.0 support is on the
16
+ works.
17
+
18
+ We are not planning to make it compatible with MRI 1.8.7 since, you know, this
19
+ would be irresponsible :)
20
+
21
+ ## Getting started
22
+
23
+ Start by adding spinach to your Gemfile:
24
+
25
+ group :test do
26
+ gem 'spinach'
27
+ # along with gem 'minitest' or gem 'rspec'
28
+ end
29
+
30
+ Spinach works with your favorite test suite, you just have to tell it which
31
+ one are you going to use in `features/support/env.rb`:
32
+
33
+ # If you want to use minitest:
34
+ require 'minitest/spec'
35
+
36
+ # If you want to use rspec:
37
+ require 'rspec'
38
+
39
+ Now create a `features` folder in your app or library and write your first
40
+ feature:
41
+
42
+ ## features/test_how_spinach_works.feature
43
+
44
+ Feature: Test how spinach works
45
+ In order to know what the heck is spinach
46
+ As a developer
47
+ I want it to behave in an expected way
48
+
49
+ Scenario: Formal greeting
50
+ Given I have an empty array
51
+ And I append my first name and my last name to it
52
+ When I pass it to my super-duper method
53
+ Then the output should contain a formal greeting
54
+
55
+ Scenario: Informal greeting
56
+ Given I have an empty array
57
+ And I append only my first name to it
58
+ When I pass it to my super-duper method
59
+ Then the output should contain a casual greeting
60
+
61
+ Now for the steps file. Remember that in Spinach steps are just Ruby classes,
62
+ following a camelcase naming convention. Spinach generator will do some
63
+ scaffolding for you:
64
+
65
+ $ spinach --generate
66
+
67
+ Spinach will detect your features and generate the following class:
68
+
69
+ ## features/steps/test_how_spinach_works.rb
70
+
71
+ class TestHowSpinachWorks < Spinach::FeatureSteps
72
+ Given 'I have an empty array' do
73
+ end
74
+
75
+ And 'I append my first name and my last name to it' do
76
+ end
77
+
78
+ When 'I pass it to my super-duper method' do
79
+ end
80
+
81
+ Then 'the output should contain a formal greeting' do
82
+ end
83
+
84
+ And 'I append only my first name to it' do
85
+ end
86
+
87
+ Then 'the output should contain a casual greeting' do
88
+ end
89
+ end
90
+
91
+ Then, you can fill it in with your logic - remember, it's just a class, you can
92
+ use private methods, mix in modules or whatever!
93
+
94
+ class TestHowSpinachWorks < Spinach::FeatureSteps
95
+ Given 'I have an empty array' do
96
+ @array = Array.new
97
+ end
98
+
99
+ And 'I append my first name and my last name to it' do
100
+ @array += ["John", "Doe"]
101
+ end
102
+
103
+ When 'I pass it to my super-duper method' do
104
+ @output = capture_output do
105
+ Greeter.greet(@array)
106
+ end
107
+ end
108
+
109
+ Then 'the output should contain a formal salutation' do
110
+ @output.must_include "Hello, mr. John Doe"
111
+ end
112
+
113
+ And 'I append only my first name to it' do
114
+ @array += ["John"]
115
+ end
116
+
117
+ Then 'the output should contain a casual salutation' do
118
+ @output.must_include "Yo, John! Whassup?"
119
+ end
120
+
121
+ private
122
+
123
+ def capture_output
124
+ out = StreamIO.new
125
+ $stdout = out
126
+ $stderr = out
127
+ yield
128
+ $stdout = STDOUT
129
+ $stderr = STDERR
130
+ out.string
131
+ end
132
+ end
133
+
134
+ Then run your feature again running `spinach` and watch it all turn green! :)
135
+
136
+ ## Contributing
137
+
138
+ You can easily contribute to Spinach. Its codebase is simple and
139
+ [extensively documented][documentation].
140
+
141
+ * Fork the project.
142
+ * Make your feature addition or bug fix.
143
+ * Add specs for it. This is important so we don't break it in a future
144
+ version unintentionally.
145
+ * Commit, do not mess with rakefile, version, or history.
146
+ If you want to have your own version, that is fine but bump version
147
+ in a commit by itself I can ignore when I pull.
148
+ * Send me a pull request. Bonus points for topic branches.
149
+
150
+ [gherkin]: http://github.com/cucumber/gherkin
151
+ [cucumber]: http://github.com/cucumber/cucumber
152
+ [documentation]: http://rubydoc.info/github/codegram/spinach/master/frames
@@ -1,4 +1,7 @@
1
- Feature 'Automatic feature generation' do
1
+ class AutomaticFeatureGeneration < Spinach::FeatureSteps
2
+
3
+ feature 'Automatic feature generation'
4
+
2
5
  include Integration::SpinachRunner
3
6
  Given 'I have defined a "Cheezburger can I has" feature' do
4
7
  write_file('features/cheezburger_can_i_has.feature',
@@ -20,7 +23,7 @@ Feature 'Automatic feature generation' do
20
23
  File.exists?(@file).must_equal true
21
24
  end
22
25
  end
23
-
26
+
24
27
  And "that feature should have the example feature steps" do
25
28
  in_current_dir do
26
29
  content = File.read(@file)
@@ -1,4 +1,7 @@
1
- Feature "Exit status" do
1
+ class ExitStatus < Spinach::FeatureSteps
2
+
3
+ feature "Exit status"
4
+
2
5
  include Integration::SpinachRunner
3
6
 
4
7
  Given "I have a feature that has no error or failure" do
@@ -9,7 +12,8 @@ Feature "Exit status" do
9
12
  Then I succeed
10
13
  ')
11
14
  write_file('features/steps/success_feature.rb',
12
- 'Feature "A success feature" do
15
+ 'class ASuccessFeature < Spinach::FeatureSteps
16
+ feature "A success feature"
13
17
  Then "I succeed" do
14
18
  end
15
19
  end')
@@ -24,7 +28,8 @@ Feature "Exit status" do
24
28
  Then I fail
25
29
  ')
26
30
  write_file('features/steps/failure_feature.rb',
27
- 'Feature "A failure feature" do
31
+ 'class AFailureFeature < Spinach::FeatureSteps
32
+ feature "A failure feature"
28
33
  Then "I fail" do
29
34
  true.must_equal false
30
35
  end
@@ -1,4 +1,7 @@
1
- Feature "Feature name guessing" do
1
+ class FeatureNameGuessing < Spinach::FeatureSteps
2
+
3
+ feature "Feature name guessing"
4
+
2
5
  include Integration::SpinachRunner
3
6
 
4
7
  Given 'I am writing a feature called "My cool feature"' do
@@ -1,6 +1,9 @@
1
1
  require 'aruba/api'
2
2
 
3
- Feature "Display run summary" do
3
+ class DisplayRunSummary < Spinach::FeatureSteps
4
+
5
+ feature 'automatic'
6
+
4
7
  include Integration::SpinachRunner
5
8
 
6
9
  Given "I have a feature that has some successful, undefined, failed and error steps" do
@@ -24,7 +27,9 @@ Feature "Display run summary" do
24
27
  Then I must succeed
25
28
  ')
26
29
  write_file('features/steps/test_feature.rb',
27
- 'Feature "A test feature" do
30
+ 'class ATestFeature < Spinach::FeatureSteps
31
+ feature "A test feature"
32
+
28
33
  Given "I am a fool" do
29
34
  end
30
35
 
@@ -65,5 +70,3 @@ Feature "Display run summary" do
65
70
  )
66
71
  end
67
72
  end
68
-
69
-
@@ -1,4 +1,7 @@
1
- Feature "Error reporting" do
1
+ class ErrorReporting < Spinach::FeatureSteps
2
+
3
+ feature "Error reporting"
4
+
2
5
  include Integration::SpinachRunner
3
6
  include Integration::ErrorReporting
4
7
 
@@ -12,7 +15,9 @@ Feature "Error reporting" do
12
15
  ')
13
16
 
14
17
  write_file('features/steps/failure_feature.rb',
15
- 'Feature "Feature with failures" do
18
+ 'class FeatureWithFailures < Spinach::FeatureSteps
19
+ feature "Feature with failures"
20
+
16
21
  Given "true is false" do
17
22
  true.must_equal false
18
23
  end
@@ -1,6 +1,9 @@
1
1
  require 'aruba/api'
2
2
 
3
- Feature "Show step source location" do
3
+ class ShowStepSourceLocation < Spinach::FeatureSteps
4
+
5
+ feature "Show step source location"
6
+
4
7
  include Integration::SpinachRunner
5
8
 
6
9
  Given "I have a feature that has no error or failure" do
@@ -11,7 +14,8 @@ Feature "Show step source location" do
11
14
  Then I succeed
12
15
  ')
13
16
  write_file('features/steps/success_feature.rb',
14
- 'Feature "A success feature" do
17
+ 'class ASuccessFeature < Spinach::FeatureSteps
18
+ feature "A success feature"
15
19
  Then "I succeed" do
16
20
  end
17
21
  end')
@@ -24,7 +28,7 @@ Feature "Show step source location" do
24
28
 
25
29
  Then "I should see the source location of each step of every scenario" do
26
30
  all_stdout.must_match(
27
- /I succeed.*features\/steps\/success_feature\.rb.*2/
31
+ /I succeed.*features\/steps\/success_feature\.rb.*3/
28
32
  )
29
33
  end
30
34
 
@@ -36,7 +40,8 @@ Feature "Show step source location" do
36
40
  Given this is a external step
37
41
  ')
38
42
  write_file('features/steps/success_feature.rb',
39
- 'Feature "A feature that uses external steps" do
43
+ 'class AFeatureThatUsesExternalSteps < Spinach::FeatureSteps
44
+ feature "A feature that uses external steps"
40
45
  include ExternalSteps
41
46
  end')
42
47
  write_file('features/support/external_steps.rb',
@@ -54,4 +59,3 @@ Feature "Show step source location" do
54
59
  )
55
60
  end
56
61
  end
57
-
@@ -1,4 +1,7 @@
1
- Feature "Undefined feature reporting" do
1
+ class UndefinedFeatureReporting < Spinach::FeatureSteps
2
+
3
+ feature "Undefined feature reporting"
4
+
2
5
  include Integration::SpinachRunner
3
6
 
4
7
  Given "I've written a feature but not its steps" do
@@ -1,4 +1,7 @@
1
- Feature "RSpec compatibility" do
1
+ class RSpecCompatibility < Spinach::FeatureSteps
2
+
3
+ feature "RSpec compatibility"
4
+
2
5
  include Integration::SpinachRunner
3
6
  include Integration::ErrorReporting
4
7
 
@@ -12,7 +15,8 @@ Feature "RSpec compatibility" do
12
15
  ')
13
16
 
14
17
  write_file('features/steps/failure_feature.rb',
15
- 'Feature "Feature with failures" do
18
+ 'class FeatureWithFailures < Spinach::FeatureSteps
19
+ feature "Feature with failures"
16
20
  Given "true is false" do
17
21
  true.should == false
18
22
  end
@@ -5,15 +5,8 @@ module Integration
5
5
  include Aruba::Api
6
6
 
7
7
  def self.included(base)
8
- base.class_eval do
9
- before_scenario do
10
- in_current_dir do
11
- FileUtils.rm_rf("features")
12
- end
13
- end
14
- before_scenario do
15
- @aruba_timeout_seconds = 6
16
- end
8
+ Spinach.hooks.before_scenario do
9
+ @aruba_timeout_seconds = 6
17
10
  end
18
11
  end
19
12
 
@@ -24,10 +24,9 @@ module Spinach
24
24
  def self.included(base)
25
25
  base.class_eval do
26
26
  include ::Capybara::DSL
27
-
28
- after_scenario do
29
- ::Capybara.current_session.reset! if ::Capybara.app
30
- end
27
+ end
28
+ Spinach.hooks.before_scenario do
29
+ ::Capybara.current_session.reset! if ::Capybara.app
31
30
  end
32
31
  end
33
32
  end
data/lib/spinach/cli.rb CHANGED
@@ -35,7 +35,6 @@ module Spinach
35
35
  def init_reporter
36
36
  reporter =
37
37
  Spinach::Reporter::Stdout.new(options[:reporter])
38
- Spinach.config.default_reporter = reporter
39
38
  reporter.bind
40
39
  end
41
40
 
@@ -45,14 +45,6 @@ module Spinach
45
45
  @support_path || 'features/support'
46
46
  end
47
47
 
48
- # The default reporter is the reporter spinach will use if there's no other
49
- # specified. Defaults to Spinach::Reporter::Stdout, which will print all
50
- # output to the standard output
51
- #
52
- def default_reporter
53
- @default_reporter || Spinach::Reporter::Stdout.new
54
- end
55
-
56
48
  # Allows you to read the config object using a hash-like syntax.
57
49
  #
58
50
  # @param [String] attribute
data/lib/spinach/dsl.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'hooks'
2
-
3
1
  module Spinach
4
2
  # Spinach DSL aims to provide an easy way to define steps and hooks into your
5
3
  # feature classes.
@@ -13,13 +11,6 @@ module Spinach
13
11
  base.class_eval do
14
12
  include InstanceMethods
15
13
  extend ClassMethods
16
- include Hooks
17
-
18
- define_hook :before_scenario
19
- define_hook :after_scenario
20
- define_hook :before_step
21
- define_hook :after_step
22
-
23
14
  end
24
15
  end
25
16
 
@@ -82,11 +73,11 @@ module Spinach
82
73
  #
83
74
  # @api public
84
75
  def execute_step(step)
85
- undercored_step = Spinach::Support.underscore(step)
76
+ underscored_step = Spinach::Support.underscore(step)
86
77
  location = nil
87
- if self.respond_to?(undercored_step)
88
- location = method(undercored_step).source_location
89
- self.send(undercored_step)
78
+ if self.respond_to?(underscored_step)
79
+ location = method(underscored_step).source_location
80
+ self.send(underscored_step)
90
81
  else
91
82
  raise Spinach::StepNotDefinedException.new(step)
92
83
  end
@@ -23,7 +23,7 @@ module Spinach
23
23
  end
24
24
 
25
25
  # This class represents the exception raised when Spinach can't find a step
26
- # for a {Scenario}.
26
+ # for a {FeatureSteps}.
27
27
  #
28
28
  class StepNotDefinedException < StandardError
29
29
  attr_reader :feature, :step
@@ -11,15 +11,7 @@ module Spinach
11
11
  #
12
12
  # @api public
13
13
  def self.inherited(base)
14
- Spinach.features << base
14
+ Spinach.feature_steps << base
15
15
  end
16
16
  end
17
17
  end
18
-
19
- # Syntactic sugar. Define the "Feature do" syntax.
20
- Object.send(:define_method, :Feature) do |name, &block|
21
- Class.new(Spinach::FeatureSteps) do
22
- feature name
23
- class_eval &block
24
- end
25
- end
@@ -1,6 +1,6 @@
1
1
  module Spinach
2
2
  module Generators
3
- # A feature generator generates and/or writes an example feature steps class
3
+ # A feature generator generates and/or writes an example feature steps class
4
4
  # given the parsed feture data
5
5
  class FeatureGenerator
6
6
 
@@ -43,7 +43,8 @@ module Spinach
43
43
  # an example feature steps definition
44
44
  def generate
45
45
  result = StringIO.new
46
- result.puts "Feature '#{Spinach::Support.escape_single_commas name}' do"
46
+ result.puts "class #{Spinach::Support.camelize name} < Spinach::FeatureSteps"
47
+ result.puts " feature \'#{Spinach::Support.escape_single_commas name}\'\n"
47
48
  generated_steps = steps.map do |step|
48
49
  step_generator = Generators::StepGenerator.new(step)
49
50
  step_generator.generate.split("\n").map do |line|
@@ -5,7 +5,7 @@ module Spinach
5
5
  module Generators
6
6
  # Binds the feature generator to the "feature not found" hook
7
7
  def self.bind
8
- Spinach::Runner::Feature.when_not_found do |data|
8
+ Spinach.hooks.on_undefined_feature do |data|
9
9
  Spinach::Generators.generate_feature(data)
10
10
  end
11
11
  end
@@ -0,0 +1,81 @@
1
+ module Spinach
2
+ # The hookable module includes subscription capabilities to the class in which
3
+ # it is included.
4
+ #
5
+ # Take in account that while most subscription/notification mechanism work
6
+ # at the class level, Hookable defines hooks at the instance level - so they
7
+ # are not the same in all the class instances.
8
+ module Hookable
9
+
10
+ def self.included(base)
11
+ base.class_eval do
12
+ extend ClassMethods
13
+ include InstanceMethods
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ # Adds a new hook to this class. Every hook defines two methods used to
19
+ # add new callbacks and to run them passing a bunch of parameters.
20
+ #
21
+ # @example
22
+ # class
23
+ def hook(hook)
24
+ define_method hook do |&block|
25
+ add_hook(hook, &block)
26
+ end
27
+ define_method "run_#{hook}" do |*args|
28
+ run_hook(hook, *args)
29
+ end
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+ attr_writer :hooks
35
+
36
+ # @return [Hash]
37
+ # hash in which the key is the hook name and the value an array of any
38
+ # defined callbacks, or nil.
39
+ def hooks
40
+ @hooks ||= {}
41
+ end
42
+
43
+ # Resets all this class' hooks to a pristine state
44
+ def reset
45
+ self.hooks = {}
46
+ end
47
+
48
+ # Runs a particular hook given a set of arguments
49
+ #
50
+ # @param [String] name
51
+ # the hook's name
52
+ #
53
+ def run_hook(name, *args)
54
+ if callbacks = hooks[name.to_sym]
55
+ callbacks.each{ |c| c.call(*args) }
56
+ end
57
+ end
58
+
59
+ # @param [String] name
60
+ # the hook's identifier
61
+ #
62
+ # @return [Array]
63
+ # array of hooks for that particular identifier
64
+ def hooks_for(name)
65
+ hooks[name.to_sym] || []
66
+ end
67
+
68
+ # Adds a hook to the queue
69
+ #
70
+ # @param [String] name
71
+ # the hook's identifier
72
+ #
73
+ # @param [Proc] block
74
+ # an action to perform once that hook is executed
75
+ def add_hook(name, &block)
76
+ hooks[name.to_sym] ||= []
77
+ hooks[name.to_sym] << block
78
+ end
79
+ end
80
+ end
81
+ end