hooker 1.0.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.
data/History.rdoc ADDED
@@ -0,0 +1,2 @@
1
+ === 1.0.0 (2011-2-6)
2
+ * Initial release.
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ History.rdoc
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/hooker/base.rb
6
+ lib/hooker.rb
7
+ test/alt_entry.rb
8
+ test/config.rb
9
+ test/setup.rb
10
+ test/test_hooker.rb
data/README.rdoc ADDED
@@ -0,0 +1,81 @@
1
+ = Hooker
2
+
3
+ * http://github.com/dekellum/hooker
4
+
5
+ == Description
6
+
7
+ Hooker provides a simple registry of hooks or extension points,
8
+ enabling decoupled run time configuration in terse but straight ruby
9
+ syntax.
10
+
11
+ Inspiration includes {Emacs Hook
12
+ Functions}[http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html]
13
+ (Lisp) and other applications which support configuration
14
+ files in ruby.
15
+
16
+ === Features
17
+
18
+ * Hook Procs may be added by the configuration source in any order.
19
+ * Hook Procs may be chained and combined via various conventions:
20
+ Hooker.apply, Hooker.inject, Hooker.merge.
21
+ * Hook Procs are executed only when applied by the extended
22
+ application. Thus a single configuration source may include hook
23
+ Procs that are left unused in certain contexts. This is useful
24
+ when configuring across several different (contextually loaded)
25
+ modules from one source.
26
+ * Optional or implicit scoping of keys, providing a two level
27
+ Hook [:scope, :key] hierarchy.
28
+ * Provides an optional logging extension point Hooker.log_with
29
+ * Can check and list hooks that were not applied, including the call
30
+ site of where added.
31
+
32
+ == Synopsis
33
+
34
+ Lets say an application has a 'Job' it would like to support
35
+ hooks on:
36
+
37
+ job = Hooker.apply( :job, Job.new )
38
+
39
+ Then the following configuration of the job could optionally be loaded
40
+ and applied to override Job defaults:
41
+
42
+ Hooker.with do |h|
43
+ h.setup_job do |j|
44
+ j.workers = 3
45
+ j.timeout = 10 * 60 #seconds
46
+ end
47
+ end
48
+
49
+ Hooker can be yielded to the config file via an alternative Module
50
+ name, so as not to scare your mom, or to fully encapsulate its use:
51
+
52
+ class Chaplain
53
+ def self.configure( &block )
54
+ Hooker.with( :church, &block )
55
+ end
56
+ end
57
+
58
+ Supports the config:
59
+
60
+ Chaplain.configure do |c|
61
+ c.setup_job do |j|
62
+ j.workers = 3
63
+ j.timeout = 10 * 60 #seconds
64
+ end
65
+ end
66
+
67
+ == License
68
+
69
+ Copyright (c) 2011 David Kellum
70
+
71
+ Licensed under the Apache License, Version 2.0 (the "License"); you
72
+ may not use this file except in compliance with the License. You
73
+ may obtain a copy of the License at:
74
+
75
+ http://www.apache.org/licenses/LICENSE-2.0
76
+
77
+ Unless required by applicable law or agreed to in writing, software
78
+ distributed under the License is distributed on an "AS IS" BASIS,
79
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
80
+ implied. See the License for the specific language governing
81
+ permissions and limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ # -*- ruby -*-
2
+
3
+ $LOAD_PATH << './lib'
4
+
5
+ require 'rubygems'
6
+ gem 'rjack-tarpit', '~> 1.3.0'
7
+ require 'rjack-tarpit'
8
+
9
+ require 'hooker/base'
10
+
11
+ t = RJack::TarPit.new( 'hooker', Hooker::VERSION )
12
+
13
+ t.specify do |h|
14
+ h.developer( 'David Kellum', 'dek-oss@gravitext.com' )
15
+ h.testlib = :minitest
16
+ h.extra_dev_deps += [ [ 'minitest', '>= 1.7.1', '< 2.1' ] ]
17
+ end
18
+
19
+ # Version/date consistency checks:
20
+
21
+ task :check_history_version do
22
+ t.test_line_match( 'History.rdoc', /^==/, / #{ t.version } / )
23
+ end
24
+ task :check_history_date do
25
+ t.test_line_match( 'History.rdoc', /^==/, /\([0-9\-]+\)$/ )
26
+ end
27
+
28
+ task :gem => [ :check_history_version ]
29
+ task :tag => [ :check_history_version, :check_history_date ]
30
+ task :push => [ :check_history_version, :check_history_date ]
31
+
32
+ t.define_tasks
@@ -0,0 +1,20 @@
1
+ #--
2
+ # Copyright (c) 2011 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You may
6
+ # obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ module Hooker
18
+ # Hooker version
19
+ VERSION = '1.0.0'
20
+ end
data/lib/hooker.rb ADDED
@@ -0,0 +1,149 @@
1
+ #--
2
+ # Copyright (c) 2011 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You may
6
+ # obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ require 'hooker/base'
18
+
19
+ # A registry of hooks, added and applied for indirect configuration
20
+ # support.
21
+ module Hooker
22
+
23
+ class << self
24
+
25
+ # Yields self under the given scope to block.
26
+ def with( scp = :default )
27
+ prior, @scope = @scope, scp
28
+ yield self
29
+ ensure
30
+ @scope = prior
31
+ end
32
+
33
+ # Add hook block by specified hook key. Will only be executed when
34
+ # apply, inject, or merge is later called with the same key.
35
+ # Multiple hook blocks for the same key will be called in the
36
+ # order added.
37
+ def add( key, clr = nil, &block )
38
+ applied.delete( sk( key ) )
39
+ hooks[ sk( key ) ] << [ block, ( clr || caller.first ).to_s ]
40
+ end
41
+
42
+ # Allow method setup_<foo> as alias for add( :foo )
43
+ def method_missing( method, *args, &block )
44
+ if method.to_s =~ /^setup_(.*)$/ && args.empty?
45
+ add( $1.to_sym, caller.first, &block )
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ # Pass the specified initial value to each previously added Proc
52
+ # with matching key and returns the mutated value.
53
+ def apply( key, value )
54
+ applied << sk( key )
55
+ hooks[ sk( key ) ].each { |hook| hook[0].call( value ) }
56
+ value
57
+ end
58
+
59
+ # Inject value (or nil) into the chain of previously added Procs,
60
+ # which should implement binary operations, returning desired
61
+ # value. Returns the last value from the last proc.
62
+ def inject( key, value = nil )
63
+ applied << sk( key )
64
+ hooks[ sk( key ) ].inject( value ) { |v, hook| hook[0].call( v ) }
65
+ end
66
+
67
+ # Merge returned values from each added Proc to the initial value
68
+ # (or empty Hash).
69
+ def merge( key, value = {} )
70
+ applied << sk( key )
71
+ hooks[ sk( key ) ].inject( value ) { |v, hook| v.merge( hook[0].call ) }
72
+ end
73
+
74
+ # Register to yield log messages to the given block.
75
+ def log_with( &block )
76
+ @logger = block
77
+ end
78
+
79
+ # Load the specified file via Kernel.load, with a log message if
80
+ # set.
81
+ def load_file( file )
82
+ log "Loading file #{file}."
83
+ load( file, true ) #wrap in in anonymous module
84
+ end
85
+
86
+ # Register -c/--config flags on given OptionParser to load_file
87
+ def register_config( opts )
88
+ opts.on( "-c", "--config FILE", "Load configuration file" ) do |file|
89
+ load_file( file )
90
+ end
91
+ end
92
+
93
+ # Log results of check_not_applied, one message per not applied
94
+ # hook.
95
+ def log_not_applied
96
+ check_not_applied do |rkey, calls|
97
+ k = rkey.map { |s| s.inspect }.compact.join( ', ' )
98
+ msg = "Hook #{k} was never applied. Added from:\n"
99
+ calls.each { |cl| msg += " - #{cl}\n" }
100
+ log msg
101
+ end
102
+ end
103
+
104
+ # Yields [ [ scope, key ], [ caller, ... ] ] to block for each
105
+ # hook key added but not applied. Often this suggests a typo or
106
+ # other mistake by the hook Proc author.
107
+ def check_not_applied
108
+ ( hooks.keys - applied ).each do |rkey|
109
+ calls = hooks[ rkey ].map { |blk, clr| clr }
110
+ yield( rkey, calls )
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ # Hash of hook keys to array of procs.
117
+ def hooks
118
+ @hooks ||= Hash.new { |h, k| h[k] = [] }
119
+ end
120
+
121
+ # List of hook keys that were applied thus far
122
+ def applied
123
+ @applied ||= []
124
+ end
125
+
126
+ # Clears all Hooker state.
127
+ def clear
128
+ @hooks = nil
129
+ @applied = nil
130
+ @logger = nil
131
+ end
132
+
133
+ def log( msg )
134
+ @logger.call( msg ) if @logger
135
+ end
136
+
137
+ def sk( key )
138
+ if key.is_a?( Array ) && ( key.length == 2 )
139
+ key
140
+ else
141
+ [ @scope, key ]
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ @scope = :default
148
+
149
+ end
data/test/alt_entry.rb ADDED
@@ -0,0 +1,5 @@
1
+ Chaplain.configure do |h|
2
+ h.setup_test do
3
+ :returned
4
+ end
5
+ end
data/test/config.rb ADDED
@@ -0,0 +1,5 @@
1
+ Hooker.with do |h|
2
+ h.setup_test do
3
+ :returned
4
+ end
5
+ end
data/test/setup.rb ADDED
@@ -0,0 +1,24 @@
1
+ #--
2
+ # Copyright (c) 2011 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You
6
+ # may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ #### General test setup: LOAD_PATH, logging, console output ####
18
+
19
+ ldir = File.join( File.dirname( __FILE__ ), "..", "lib" )
20
+ $LOAD_PATH.unshift( ldir ) unless $LOAD_PATH.include?( ldir )
21
+
22
+ require 'rubygems'
23
+ require 'minitest/unit'
24
+ require 'minitest/autorun'
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env jruby
2
+ #.hashdot.profile += jruby-shortlived
3
+ #--
4
+ # Copyright (c) 2011 David Kellum
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
7
+ # may not use this file except in compliance with the License. You
8
+ # may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15
+ # implied. See the License for the specific language governing
16
+ # permissions and limitations under the License.
17
+ #++
18
+
19
+ TESTDIR = File.dirname( __FILE__ )
20
+
21
+ require File.join( TESTDIR, "setup" )
22
+
23
+ require 'hooker'
24
+
25
+ # An alternative entry point from top level
26
+ class Chaplain
27
+ def self.configure( &block )
28
+ Hooker.with( :church, &block )
29
+ end
30
+ end
31
+
32
+ class TestContext < MiniTest::Unit::TestCase
33
+
34
+ def setup
35
+ Hooker.log_with { |msg| puts; puts msg }
36
+ end
37
+
38
+ def teardown
39
+ Hooker.send( :clear ) #private
40
+ end
41
+
42
+ def test_not_set
43
+ assert_nil( Hooker.inject( :test_hook ) )
44
+
45
+ assert_equal( :identity, Hooker.inject( :test_hook, :identity ) )
46
+ end
47
+
48
+ def test_no_arg_hook
49
+ Hooker.add( :test ) { :returned }
50
+
51
+ assert_equal( :returned, Hooker.inject( :test ) )
52
+ assert_equal( :returned, Hooker.inject( :test, :ignored ) )
53
+ end
54
+
55
+ def test_chained_hook
56
+ Hooker.with do |h|
57
+ h.add( :test ) { |l| l << :a }
58
+ h.add( :test ) { |l| l << :b }
59
+ end
60
+
61
+ assert_equal( [ :a, :b ], Hooker.inject( :test, [] ) )
62
+ end
63
+
64
+ def test_setup_method
65
+ Hooker.setup_test { :returned } # via method_missing
66
+ assert_equal( :returned, Hooker.inject( :test ) )
67
+ end
68
+
69
+ def test_not_setup_missing
70
+ assert_raises( NoMethodError ) do
71
+ Hooker.bogus_method
72
+ end
73
+ end
74
+
75
+ def test_apply
76
+ Hooker.with do |h|
77
+ h.add( :test ) { |h| h[ :prop ] = "a" }
78
+ h.add( :test ) { |h| h[ :prop ] = "b" }
79
+ end
80
+
81
+ h = Hooker.apply( :test, { :prop => "orig" } )
82
+
83
+ assert_equal( "b", h[ :prop ] )
84
+ end
85
+
86
+ def test_merge
87
+ Hooker.add( :test ) { { :a => 1, :b => 2 } }
88
+ Hooker.add( :test ) { { :c => 3 } }
89
+
90
+ h = Hooker.merge( :test,
91
+ { :a => 0, :d => 4 } )
92
+ assert_equal( h, { :a => 1, :b => 2, :c => 3, :d => 4 } )
93
+ end
94
+
95
+ def test_check_not_applied
96
+
97
+ Hooker.with( :test_scope ) do |h|
98
+ h.add( :used ) { :returned }
99
+ h.add( :not_used ) { flunk "first time" }
100
+ h.add( :not_used ) { flunk "once more" }
101
+ end
102
+
103
+ assert_equal( :returned,
104
+ Hooker.inject( [ :test_scope, :used ] ) )
105
+
106
+ not_used_keys = []
107
+ not_used_calls = []
108
+ Hooker.check_not_applied do |rkey, calls|
109
+ not_used_keys << rkey
110
+ not_used_calls += calls
111
+ end
112
+
113
+ assert_equal( [ [ :test_scope, :not_used ] ], not_used_keys )
114
+ assert_equal( 2, not_used_calls.length )
115
+
116
+ Hooker.log_not_applied
117
+ end
118
+
119
+ def test_check_not_applied_if_added_after
120
+
121
+ Hooker.with( :test_scope ) do |h|
122
+ assert_nil( h.inject( :not_used ) )
123
+ h.add( :not_used ) { :returned }
124
+
125
+ not_used_keys = []
126
+ not_used_keys = Hooker.check_not_applied do |rkey, calls|
127
+ not_used_keys << rkey
128
+ end
129
+ assert_equal( [ [:test_scope, :not_used] ], not_used_keys )
130
+ end
131
+
132
+ end
133
+
134
+ def test_load
135
+ Hooker.load_file( File.join( TESTDIR, 'config.rb' ) )
136
+ assert_equal( :returned, Hooker.inject( :test ) )
137
+ end
138
+
139
+ def test_load_with_alt_entry
140
+ Hooker.load_file( File.join( TESTDIR, 'alt_entry.rb' ) )
141
+ assert_equal( :returned, Hooker.inject( [ :church, :test ] ) )
142
+ end
143
+
144
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hooker
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - David Kellum
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-02-06 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: minitest
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 7
30
+ - 1
31
+ version: 1.7.1
32
+ - - <
33
+ - !ruby/object:Gem::Version
34
+ segments:
35
+ - 2
36
+ - 1
37
+ version: "2.1"
38
+ type: :development
39
+ version_requirements: *id001
40
+ - !ruby/object:Gem::Dependency
41
+ name: rjack-tarpit
42
+ prerelease: false
43
+ requirement: &id002 !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ segments:
48
+ - 1
49
+ - 3
50
+ - 0
51
+ version: 1.3.0
52
+ type: :development
53
+ version_requirements: *id002
54
+ description: |-
55
+ Hooker provides a simple registry of hooks or extension points,
56
+ enabling decoupled run time configuration in terse but straight ruby
57
+ syntax.
58
+
59
+ Inspiration includes {Emacs Hook
60
+ Functions}[http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html]
61
+ (Lisp) and other applications which support configuration
62
+ files in ruby.
63
+ email:
64
+ - dek-oss@gravitext.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files:
70
+ - Manifest.txt
71
+ - History.rdoc
72
+ - README.rdoc
73
+ files:
74
+ - History.rdoc
75
+ - Manifest.txt
76
+ - README.rdoc
77
+ - Rakefile
78
+ - lib/hooker/base.rb
79
+ - lib/hooker.rb
80
+ - test/alt_entry.rb
81
+ - test/config.rb
82
+ - test/setup.rb
83
+ - test/test_hooker.rb
84
+ has_rdoc: true
85
+ homepage: http://github.com/dekellum/hooker
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options:
90
+ - --main
91
+ - README.rdoc
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ requirements: []
109
+
110
+ rubyforge_project: hooker
111
+ rubygems_version: 1.3.6
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Hooker provides a simple registry of hooks or extension points, enabling decoupled run time configuration in terse but straight ruby syntax
115
+ test_files:
116
+ - test/test_hooker.rb