hooker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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