hooker 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5b0d8be31cce91bd8b60f4e0bd5ceaa29ae29062
4
+ data.tar.gz: d3fcaef32aad0b808a9207dda29e1b7cf9567bb1
5
+ SHA512:
6
+ metadata.gz: f4db4e60785200caf839f3b0560ad6c16a07e943d4fed0c65670c028734d615b59ecd26baf67b5bda25b5784749a022cb05354d6fbc5ae336f98db5f44c7e0bb
7
+ data.tar.gz: 9a1ad28dc7f66176f512b26e11c02511749cf00d716330438ef9178313f1814a70c2c32954e6130a45a1b1633f9d9ecf2fbd6796918199831f16c49e4f8f3aee
@@ -1,3 +1,11 @@
1
+ === 1.1.0 (2013-12-6)
2
+ * Thread safety: Hooker.with via thread-local and lock around common
3
+ structures. Note: It remains generally advisable to complete all
4
+ configurable application bootstrapping in the main thread whenever
5
+ possible.
6
+ * Add Travis CI test setup (dev)
7
+ * Upgrade to minitest ~> 4.7.4 (dev)
8
+
1
9
  === 1.0.1 (2012-9-12)
2
10
  * Ensure Errno::EISDIR when mistaken attempt to load a directory,
3
11
  i.e. "./config"
@@ -1,6 +1,7 @@
1
1
  = Hooker
2
2
 
3
3
  * http://github.com/dekellum/hooker
4
+ * {<img src="https://travis-ci.org/dekellum/hooker.png" />}[https://travis-ci.org/dekellum/hooker]
4
5
 
5
6
  == Description
6
7
 
@@ -17,24 +18,37 @@ and other ruby-based configuration files.
17
18
  Hooker.apply, Hooker.inject, Hooker.merge.
18
19
  * Hook Procs are executed only when applied by the extended
19
20
  application. Thus a single configuration source may include hook
20
- Procs that are left unused in certain contexts. This is useful
21
+ Procs that are left un-executed in certain contexts. This is useful
21
22
  when configuring across several different (contextually loaded)
22
23
  modules from one source.
23
- * Optional or implicit scoping of keys, providing a two level
24
- Hook [:scope, :key] hierarchy.
25
- * Provides an optional logging extension point Hooker.log_with
24
+ * Optional or implicit scoping of keys, providing a two level Hook
25
+ <code>[:scope, :key]</code> hierarchy. The default scope is
26
+ <code>:default</code>.
27
+ * Optional Hooker.log_with extension point
26
28
  * Can check and list hooks that were not applied, including the call
27
- site of where added.
29
+ site of where added, allowing you to test your
30
+ configurations and avoid typos.
31
+ * Thread safe (though you should strive to complete all configurable
32
+ bootstrapping in the main thread).
28
33
 
29
34
  == Synopsis
30
35
 
31
36
  Lets say an application has a 'Job' it would like to support
32
- hooks on:
37
+ configuration hooks on. First arrange for any number of configuration
38
+ sources to be loaded via Hooker.load_file. Then apply any loaded hook
39
+ procs to an instance of your Job, like so:
33
40
 
41
+ require 'hooker'
42
+
43
+ Hooker.load_file( "config.rb" ) if File.exist?( "config.rb" )
34
44
  job = Hooker.apply( :job, Job.new )
35
45
 
36
- Then the following configuration of the job could optionally be loaded
37
- and applied to override Job defaults:
46
+ You could also use Hooker.register_config with an OptionParser to
47
+ support a <code>-c/--config FILE</code> flag for specifying the
48
+ configuration source.
49
+
50
+ If Job is to be configured via setters, then the configuration source
51
+ might look like this:
38
52
 
39
53
  Hooker.with do |h|
40
54
  h.setup_job do |j|
@@ -43,8 +57,32 @@ and applied to override Job defaults:
43
57
  end
44
58
  end
45
59
 
46
- Hooker can be yielded to the config file via an alternative Module
47
- name, so as not to scare your mom, or to fully encapsulate its use:
60
+ Alternatively, if Job takes a Hash on construction for configuration,
61
+ use Hooker.merge like so:
62
+
63
+ opts = Hooker.merge( :job, { workers: 2 } ) #defaults
64
+ job = Job.new( opts )
65
+
66
+ ...and Hash syntax in the configuration source. Note that unlike JSON
67
+ or YAML, the configuration remains fully interpreted for greater
68
+ expressiveness, for example sharing variables across different
69
+ configured objects:
70
+
71
+ Hooker.with do |h|
72
+ cpus = 3
73
+
74
+ h.setup_job do
75
+ { workers: cpus + 1,
76
+ timeout: 10 * 60 }
77
+ end
78
+
79
+ h.setup_connection_pool do
80
+ { size: cpus }
81
+ end
82
+ end
83
+
84
+ Hooker can be yielded to the configuration source via an alternative
85
+ Module.method name, to fully encapsulate its use:
48
86
 
49
87
  class Chaplain
50
88
  def self.configure( &block )
@@ -52,7 +90,7 @@ name, so as not to scare your mom, or to fully encapsulate its use:
52
90
  end
53
91
  end
54
92
 
55
- Supports the config:
93
+ ...supports the configuration source:
56
94
 
57
95
  Chaplain.configure do |c|
58
96
  c.setup_job do |j|
@@ -63,7 +101,7 @@ Supports the config:
63
101
 
64
102
  == License
65
103
 
66
- Copyright (c) 2011-2012 David Kellum
104
+ Copyright (c) 2011-2013 David Kellum
67
105
 
68
106
  Licensed under the Apache License, Version 2.0 (the "License"); you
69
107
  may not use this file except in compliance with the License. You
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2011-2012 David Kellum
2
+ # Copyright (c) 2011-2013 David Kellum
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License"); you
5
5
  # may not use this file except in compliance with the License. You may
@@ -15,6 +15,7 @@
15
15
  #++
16
16
 
17
17
  require 'hooker/base'
18
+ require 'thread'
18
19
 
19
20
  # A registry of hooks, added and applied for indirect configuration
20
21
  # support.
@@ -24,19 +25,23 @@ module Hooker
24
25
 
25
26
  # Yields self under the given scope to block.
26
27
  def with( scp = :default )
27
- prior, @scope = @scope, scp
28
+ prior = swap_scope( scp )
28
29
  yield self
29
30
  ensure
30
- @scope = prior
31
+ swap_scope( prior )
31
32
  end
32
33
 
33
34
  # Add hook block by specified hook key. Will only be executed when
34
35
  # apply, inject, or merge is later called with the same key.
35
36
  # Multiple hook blocks for the same key will be called in the
36
37
  # order added.
37
- def add( key, clr = nil, &block )
38
- applied.delete( sk( key ) )
39
- hooks[ sk( key ) ] << [ block, ( clr || caller.first ).to_s ]
38
+ def add( k, clr = nil, &block )
39
+ key = sk( k )
40
+ clr ||= caller.first
41
+ LOCK.synchronize do
42
+ applied.delete( key )
43
+ hooks[ key ] << [ block, clr.to_s ]
44
+ end
40
45
  end
41
46
 
42
47
  # Allow method setup_<foo> as alias for add( :foo )
@@ -51,24 +56,27 @@ module Hooker
51
56
  # Pass the specified initial value to each previously added Proc
52
57
  # with matching key and returns the mutated value.
53
58
  def apply( key, value )
54
- applied << sk( key )
55
- hooks[ sk( key ) ].each { |hook| hook[0].call( value ) }
56
- value
59
+ sync_on_hooks( key ) do |hks|
60
+ hks.each { |hook| hook[0].call( value ) }
61
+ value
62
+ end
57
63
  end
58
64
 
59
65
  # Inject value (or nil) into the chain of previously added Procs,
60
66
  # which should implement binary operations, returning desired
61
67
  # value. Returns the last value from the last proc.
62
68
  def inject( key, value = nil )
63
- applied << sk( key )
64
- hooks[ sk( key ) ].inject( value ) { |v, hook| hook[0].call( v ) }
69
+ sync_on_hooks( key ) do |hks|
70
+ hks.inject( value ) { |v, hook| hook[0].call( v ) }
71
+ end
65
72
  end
66
73
 
67
74
  # Merge returned values from each added Proc to the initial value
68
75
  # (or empty Hash).
69
76
  def merge( key, value = {} )
70
- applied << sk( key )
71
- hooks[ sk( key ) ].inject( value ) { |v, hook| v.merge( hook[0].call ) }
77
+ sync_on_hooks( key ) do |hks|
78
+ hks.inject( value ) { |v, hook| v.merge( hook[0].call ) }
79
+ end
72
80
  end
73
81
 
74
82
  # Register to yield log messages to the given block.
@@ -109,14 +117,18 @@ module Hooker
109
117
  # hook key added but not applied. Often this suggests a typo or
110
118
  # other mistake by the hook Proc author.
111
119
  def check_not_applied
112
- ( hooks.keys - applied ).each do |rkey|
113
- calls = hooks[ rkey ].map { |blk, clr| clr }
114
- yield( rkey, calls )
120
+ LOCK.synchronize do
121
+ ( hooks.keys - applied ).each do |rkey|
122
+ calls = hooks[ rkey ].map { |blk, clr| clr }
123
+ yield( rkey, calls )
124
+ end
115
125
  end
116
126
  end
117
127
 
118
128
  private
119
129
 
130
+ LOCK = Mutex.new
131
+
120
132
  # Hash of hook keys to array of procs.
121
133
  def hooks
122
134
  @hooks ||= Hash.new { |h, k| h[k] = [] }
@@ -129,9 +141,11 @@ module Hooker
129
141
 
130
142
  # Clears all Hooker state.
131
143
  def clear
132
- @hooks = nil
133
- @applied = nil
134
- @logger = nil
144
+ LOCK.synchronize do
145
+ @hooks = nil
146
+ @applied = nil
147
+ @logger = nil
148
+ end
135
149
  end
136
150
 
137
151
  def log( msg )
@@ -142,12 +156,32 @@ module Hooker
142
156
  if key.is_a?( Array ) && ( key.length == 2 )
143
157
  key
144
158
  else
145
- [ @scope, key ]
159
+ [ scope, key ]
146
160
  end
147
161
  end
148
162
 
149
- end
163
+ def scope
164
+ Thread.current[:hooker_scope] || :default
165
+ end
150
166
 
151
- @scope = :default
167
+ def swap_scope( s )
168
+ old = scope
169
+ Thread.current[:hooker_scope] = s
170
+ old
171
+ end
172
+
173
+ def sync_on_hooks( k )
174
+ key = sk( k )
175
+ LOCK.synchronize do
176
+ begin
177
+ yield hooks[ key ]
178
+ ensure
179
+ applied << key
180
+ end
181
+ end
182
+
183
+ end
184
+
185
+ end
152
186
 
153
187
  end
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2011-2012 David Kellum
2
+ # Copyright (c) 2011-2013 David Kellum
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License"); you
5
5
  # may not use this file except in compliance with the License. You may
@@ -16,5 +16,5 @@
16
16
 
17
17
  module Hooker
18
18
  # Hooker version
19
- VERSION = '1.0.1'
19
+ VERSION = '1.1.0'
20
20
  end
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2011-2012 David Kellum
2
+ # Copyright (c) 2011-2013 David Kellum
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License"); you
5
5
  # may not use this file except in compliance with the License. You
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env jruby
2
2
  #.hashdot.profile += jruby-shortlived
3
3
  #--
4
- # Copyright (c) 2011-2012 David Kellum
4
+ # Copyright (c) 2011-2013 David Kellum
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License"); you
7
7
  # may not use this file except in compliance with the License. You
@@ -21,6 +21,7 @@ TESTDIR = File.dirname( __FILE__ )
21
21
  require File.join( TESTDIR, "setup" )
22
22
 
23
23
  require 'hooker'
24
+ require 'thread'
24
25
 
25
26
  # An alternative entry point from top level
26
27
  class Chaplain
@@ -112,6 +113,9 @@ class TestContext < MiniTest::Unit::TestCase
112
113
 
113
114
  assert_equal( [ [ :test_scope, :not_used ] ], not_used_keys )
114
115
  assert_equal( 2, not_used_calls.length )
116
+ not_used_calls.each_with_index do |cl, i|
117
+ assert_match( /test_check_not_applied/, cl, i )
118
+ end
115
119
 
116
120
  Hooker.log_not_applied
117
121
  end
@@ -147,4 +151,22 @@ class TestContext < MiniTest::Unit::TestCase
147
151
  end
148
152
  end
149
153
 
154
+ def test_threaded_with
155
+ Hooker.with( :t1 ) { |h| h.add( :common ) { :t1 } }
156
+ Hooker.with( :t2 ) { |h| h.add( :common ) { :t2 } }
157
+
158
+ threads = 11.times.map do |i|
159
+ Thread.new do
160
+ Hooker.with( i.even? ? :t1 : :t2 ) do
161
+ sleep( rand * 0.100 )
162
+ Hooker.inject( :common )
163
+ end
164
+ end
165
+ end
166
+
167
+ 11.times do |i|
168
+ assert_equal( i.even? ? :t1 : :t2, threads[i].value, "thread #{i}" )
169
+ end
170
+ end
171
+
150
172
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hooker
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 1.0.1
4
+ version: 1.1.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - David Kellum
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-09-13 00:00:00.000000000 Z
11
+ date: 2013-12-06 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: minitest
@@ -17,14 +16,12 @@ dependencies:
17
16
  requirements:
18
17
  - - ~>
19
18
  - !ruby/object:Gem::Version
20
- version: '2.3'
21
- none: false
19
+ version: 4.7.4
22
20
  requirement: !ruby/object:Gem::Requirement
23
21
  requirements:
24
22
  - - ~>
25
23
  - !ruby/object:Gem::Version
26
- version: '2.3'
27
- none: false
24
+ version: 4.7.4
28
25
  prerelease: false
29
26
  type: :development
30
27
  - !ruby/object:Gem::Dependency
@@ -33,14 +30,12 @@ dependencies:
33
30
  requirements:
34
31
  - - ~>
35
32
  - !ruby/object:Gem::Version
36
- version: '2.0'
37
- none: false
33
+ version: '2.1'
38
34
  requirement: !ruby/object:Gem::Requirement
39
35
  requirements:
40
36
  - - ~>
41
37
  - !ruby/object:Gem::Version
42
- version: '2.0'
43
- none: false
38
+ version: '2.1'
44
39
  prerelease: false
45
40
  type: :development
46
41
  description: Hooker provides a simple registry of hooks or extension points, enabling decoupled run time configuration in terse but straight ruby syntax. Inspiration includes Emacs Hook Functions and other ruby-based configuration files.
@@ -65,6 +60,7 @@ files:
65
60
  - test/test_load_dir/placeholder
66
61
  homepage: http://github.com/dekellum/hooker
67
62
  licenses: []
63
+ metadata: {}
68
64
  post_install_message:
69
65
  rdoc_options:
70
66
  - --main
@@ -73,27 +69,18 @@ require_paths:
73
69
  - lib
74
70
  required_ruby_version: !ruby/object:Gem::Requirement
75
71
  requirements:
76
- - - ! '>='
72
+ - - '>='
77
73
  - !ruby/object:Gem::Version
78
74
  version: '0'
79
- segments:
80
- - 0
81
- hash: 2
82
- none: false
83
75
  required_rubygems_version: !ruby/object:Gem::Requirement
84
76
  requirements:
85
- - - ! '>='
77
+ - - '>='
86
78
  - !ruby/object:Gem::Version
87
79
  version: '0'
88
- segments:
89
- - 0
90
- hash: 2
91
- none: false
92
80
  requirements: []
93
81
  rubyforge_project:
94
- rubygems_version: 1.8.24
82
+ rubygems_version: 2.1.9
95
83
  signing_key:
96
- specification_version: 3
84
+ specification_version: 4
97
85
  summary: Hooker provides a simple registry of hooks or extension points, enabling decoupled run time configuration in terse but straight ruby syntax.
98
86
  test_files: []
99
- ...