group_delegator 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.3.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.5.2"
12
+ gem "rcov", ">= 0"
13
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 David Martin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,120 @@
1
+ = group_delegator
2
+
3
+ GroupDelegator provides a way to wrap a collection of objects and send method calls to the entire collection.
4
+ * Simple case: Bind a collection of existing objects to a common wrapper that will proxy the method calls to each object
5
+ Example:
6
+ require 'group_delegator'
7
+
8
+ proxy_numbers = ["1", "2", "3"]
9
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
10
+ proxy_data = proxy_all.to_i
11
+ #=> {"1"=>1, "2"=>2, "3"=>3}
12
+
13
+ #or if you just wanted the integers
14
+ proxy_integers = proxy_all.to_i
15
+ #=> [1, 2, 3]
16
+ puts "proxied to_i: #{proxy_integers.values.inspect}"
17
+
18
+ * Why not just use Array#map to do the same transformation? i.e.:
19
+ map_numbers =["1", "2", "3"]
20
+ mapped_integers = map_numbers.map{|t| t.to_i}
21
+ puts "mapped to_i: #{mapped_integers.inspect}"
22
+
23
+ * Let's compare the two approaches in a bit of detail
24
+ #proxy by group_delegator
25
+ proxy_numbers = ["1", "2", "3"]
26
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
27
+ proxy_integers = proxy_all.to_i
28
+ #lets add the string "times" to each number
29
+ proxy_string = proxy_all<< " times"
30
+ #then make it uppercase
31
+ proxy_upcase = proxy_all.upcase
32
+
33
+ #tranform collection by Array#map
34
+ map_numbers =["1", "2", "3"]
35
+ map_integers = map_numbers.map{|t| t.to_i}
36
+ #lets add the string "times" to each number
37
+ map_string = map_numbers.map{|t| t << " times"}
38
+ #then make it uppercase
39
+ map_upcase = map_string.map{|t| t.upcase}
40
+
41
+ #proxy output
42
+ puts "Proxy: #{proxy_integers.values.inspect}"
43
+ puts "Proxy: #{proxy_string.values.inspect}"
44
+ puts "Proxy: #{proxy_upcase.values.inspect}"
45
+ #=> Proxy: [1, 2, 3]
46
+ #=> Proxy: ["1 times", "2 times", "3 times"]
47
+ #=> Proxy: ["1 TIMES", "2 TIMES", "3 TIMES"]
48
+
49
+ #map output
50
+ puts "Map: #{map_integers.inspect}"
51
+ puts "Map: #{map_string.inspect}"
52
+ puts "Map: #{map_upcase.inspect}"
53
+ #=> Map: [1, 2, 3]
54
+ #=> Map: ["1 times", "2 times", "3 times"]
55
+ #=> Map: ["1 TIMES", "2 TIMES", "3 TIMES"]
56
+
57
+ * Although we get to the same point at the end, the route getting there is quite a bit different between the two approaches. Also, I use this example, not because it illustrates how GroupDelegator is useful, but to relate its behavior to something relatively common.
58
+
59
+ Things to notice:
60
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
61
+ This will have all methods called on proxy_all, to be forward to each object in the proxy_numbers array. Any method on <i>proxy_all</i> is applied to each object in <i>proxy_numbers</i>, however, each object does not have to respond to the method (as long as at least one does).
62
+
63
+ proxy_all.to_i
64
+ Apply the method #to_i to all members of proxy_numbers. Which brings us to a feature mentioned above. Say our original array was:
65
+ ["1", "2", "3", :a, "b", "cat"]
66
+ With SimpleDelegator, calling #to_i results in a hash, and the values of that hash would be:
67
+ [1, 2, 3, 0, 0]
68
+ On the other hand, with Array#map, the result is a NoMethodError.
69
+
70
+ What is the structure of the GroupDelegator hash then? It's the source object as the key, with the method result as the value, so #to_i on a SimpleDelegator object returns
71
+ {"1" => 1, "2" => 2, "3" => 3, "b" => 0, "cat" => 0}
72
+ Notice :a is not in the hash. That's because it errored out. Errors are ignored by GroupDelegator (SimpleDelegator is a type of GroupDelegator).
73
+
74
+
75
+ * If we are able to work around troublesome objects, it might be useful to identify them
76
+
77
+ #Group Delegator proxying
78
+ proxy_numbers = ["1", "2", "3", :a, "b", "cat"]
79
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
80
+ proxy_integers = proxy_all.to_i
81
+
82
+ #find trouble makers
83
+ trouble_makers = proxy_numbers - proxy_integers.keys
84
+ #=> [:a]
85
+
86
+ == Beyond the basics
87
+ So up to now I've made it look like GroupDelegator is just a slightly better alternative to Array#map. But there's some more advanced things that GroupDelegator can do. For example
88
+ * Multiple Concurrenty models. The default model is to iterate over object in the collection that's being proxied, but also supported is to spawn a thread for each object, resulting in each method being called to each object concurrently. Especially useful for method calls that might take some time (like http requests). Additionally, it may be useful in certain cases that all is needed is a single response from any object. That too is supported. Finally, it's possible to pass your own block into a GroupDelegator if some other concurrency model is required.
89
+ * Class level delegation. The SimpleGroupDelegator class works on objects, but if you need to proxy an entire class, you can use the GroupDelegator class. Calling #new on the proxy class instantiates an object from each of the proxied classes, and returns a proxy_object. That proxy object then works similarly to a SimpleGroupDelegator object.
90
+
91
+ == Future Plans
92
+ * add new Delegator -> GroupMetaDelegator that passes *all* methods through to sources (including methods from Object and Module). This requires remapping existing default methods to an alias (probably # __gd__method_name)
93
+ * add a method that returns the underlying objects.
94
+ proxy_all.tap {|s| s}
95
+ #=> {obj1 => obj1, obj2 => obj2, etc}
96
+ As shown above Object#tap can do this, but perhaps a better solution would be:
97
+ proxy_all.self
98
+ #=> [obj1, obj2, etc]
99
+ * Consider adding a helper method for finding objects that didn't respond to a method. I'm not quite sure how to do this though (or even if its possible). Note, I don't want a #respond_to? clone, and re-doing the method isn't a good solution since the ojbect state may have changed (affecting its ability to respond to methods)
100
+
101
+ == Examples on Use (until I can get a full fledged tutorial written up)
102
+ Several examples (included those referenced here) are in the examples directory.
103
+ There's also the specs. Some of the examples and specs have benchmarks showing the differences between the various concurrency models.
104
+
105
+
106
+ == Contributing to group_delegator
107
+
108
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
109
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
110
+ * Fork the project
111
+ * Start a feature/bugfix branch
112
+ * Commit and push until you are happy with your contribution
113
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
114
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
115
+
116
+ == Copyright
117
+
118
+ Copyright (c) 2011 David Martin See LICENSE.txt for
119
+ further details.
120
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "group_delegator"
16
+ gem.homepage = "http://github.com/forforf/group_delegator"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Delegate to multiple objects concurrently}
19
+ gem.description = %Q{A wrapper that allows method calls to multiple objects with various concurrency models}
20
+ gem.email = "dmarti21@gmail.com"
21
+ gem.authors = ["Dave M"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "group_delegator #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,50 @@
1
+ require 'group_delegator'
2
+ proxy_numbers = ["1", "2", "3"]
3
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
4
+ proxy_data = proxy_all.to_i
5
+ #=> {"1"=>1, "2"=>2, "3"=>3}
6
+ #or if you just wanted the integers
7
+ proxy_integers = proxy_all.to_i
8
+ #=> [1, 2, 3]
9
+ puts "proxied to_i: #{proxy_integers.values.inspect}"
10
+
11
+ #Why not just use map? i.e.:
12
+ map_numbers =["1", "2", "3"]
13
+ mapped_integers = map_numbers.map{|t| t.to_i}
14
+ puts "mapped to_i: #{mapped_integers.inspect}"
15
+
16
+ #Let's compare
17
+ #Group Delegator proxying
18
+ proxy_numbers = ["1", "2", "3"]
19
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
20
+ proxy_integers = proxy_all.to_i
21
+ #lets add the string "times" to each number
22
+ proxy_string = proxy_all<< " times"
23
+ #then make it uppercase
24
+ proxy_upcase = proxy_all.upcase
25
+
26
+ #map
27
+ map_numbers =["1", "2", "3"]
28
+ map_integers = map_numbers.map{|t| t.to_i}
29
+ #lets add the string "times" to each number
30
+ map_string = map_numbers.map{|t| t << " times"}
31
+ #then make it uppercase
32
+ map_upcase = map_string.map{|t| t.upcase}
33
+
34
+
35
+ #proxy output
36
+ puts "Proxy: #{proxy_integers.values.inspect}"
37
+ puts "Proxy: #{proxy_string.values.inspect}"
38
+ puts "Proxy: #{proxy_upcase.values.inspect}"
39
+ #=> Proxy: [1, 2, 3]
40
+ #=> Proxy: ["1 times", "2 times", "3 times"]
41
+ #=> Proxy: ["1 TIMES", "2 TIMES", "3 TIMES"]
42
+
43
+ #map output
44
+ puts "Map: #{map_integers.inspect}"
45
+ puts "Map: #{map_string.inspect}"
46
+ puts "Map: #{map_upcase.inspect}"
47
+ #=> Map: [1, 2, 3]
48
+ #=> Map: ["1 times", "2 times", "3 times"]
49
+ #=> Map: ["1 TIMES", "2 TIMES", "3 TIMES"]
50
+
@@ -0,0 +1,21 @@
1
+ require 'group_delegator'
2
+ #Note the last elements of the array
3
+ proxy_numbers = ["1", "2", "3", :a, "b", "cat"]
4
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
5
+ proxy_data = proxy_all.to_i
6
+ #=> {"1"=>1, "2"=>2, "3"=>3}
7
+ #or if you just wanted the integers
8
+ proxy_integers = proxy_all.to_i
9
+ #=> [1, 2, 3]
10
+ puts "proxied to_i: #{proxy_integers.values.inspect}"
11
+
12
+ #Why not just use map? i.e.:
13
+ map_numbers =["1", "2", "3", :a, "b", "cat"]
14
+ begin
15
+ mapped_integers = map_numbers.map{|t| t.to_i}
16
+ rescue NoMethodError
17
+ puts "We just rescued a NoMehodError in a #map method call"
18
+ #=> NoMethod Error
19
+ end
20
+ puts "mapped to_i: #{mapped_integers.inspect}"
21
+
@@ -0,0 +1,12 @@
1
+ require 'group_delegator'
2
+
3
+ #Group Delegator proxying
4
+ proxy_numbers = ["1", "2", "3", :a, "b", "cat"]
5
+ proxy_all = SimpleGroupDelegator.new(proxy_numbers)
6
+ proxy_integers = proxy_all.to_i
7
+
8
+ #find trouble makers
9
+ trouble_makers = proxy_numbers - proxy_integers.keys
10
+ #=> [:a]
11
+
12
+
@@ -0,0 +1,47 @@
1
+ require 'group_delegator'
2
+
3
+
4
+ #Imagine we have many objects
5
+ class MyRemoteComponent
6
+ attr_accessor :status, :version
7
+
8
+ def remote_update(version)
9
+ sleep 0.1 #they're slow to update
10
+ @version = version
11
+ end
12
+
13
+ def
14
+ end
15
+
16
+ components = []
17
+ 100.times do
18
+ stat = [:ok, :minor, :major, :fail][rand(4)]
19
+ mycomp = MyRemoteComponent.new
20
+ mycomp.status = [:ok, :minor, :major, :fail][rand(4)]
21
+ components << mycomp
22
+ end
23
+
24
+ #let's get the status from each component
25
+
26
+ #this proxyies all components' methods into a single wrapper
27
+ all_as_one = SimpleGroupDelegator.new(components)
28
+
29
+ #so we can check the status of all of them in a single line
30
+ all_status = all_as_one.status
31
+
32
+ #the return result is a hash of the {obj1 => result1, obj2 => result2, etc}
33
+ #if we don't care about the obj we can just grab the values
34
+ p all_status.values
35
+
36
+ all_as_one.remote_update("v1.0.0")
37
+
38
+ p all_as_one.version.values.uniq
39
+ #=> ["v1.0.0"] but that was kinda slow
40
+
41
+ #lets do them all in parallel then
42
+ all_as_one_v2 = SimpleGroupDelegator.new(components, :threaded)
43
+
44
+ all_as_one_v2.remote_update("v2.0.0")
45
+
46
+ p all_as_one_v2.version.values.uniq
47
+
@@ -0,0 +1,40 @@
1
+ require 'group_delegator'
2
+ require 'open-uri'
3
+ require 'benchmark'
4
+ class Bing
5
+ QueryString = "http://www.bing.com/search?q="
6
+ def search(search_string)
7
+ url = QueryString + URI.escape(search_string)
8
+ open(url){|f| f.meta}
9
+ end
10
+ end
11
+
12
+ class Google
13
+ QueryString = "http://www.google.com/search?q="
14
+ def search(search_string)
15
+ url = QueryString + URI.escape(search_string)
16
+ open(url){|f| f.meta}
17
+ end
18
+ end
19
+
20
+ class Yahoo
21
+ QueryString = "http://search.yahoo.com/search?p="
22
+ def search(search_string)
23
+ url = QueryString + URI.escape(search_string)
24
+ open(url){|f| f.meta}
25
+ end
26
+ end
27
+
28
+ #default is iterative searchclass IterativeSearch < SimpleDel
29
+ searchers = [Bing.new, Google.new, Yahoo.new]
30
+ search_iteratively = SimpleGroupDelegator.new(searchers)
31
+ search_threaded = SimpleGroupDelegator.new(searchers, :threaded)
32
+ search_first_response = SimpleGroupDelegator.new(searchers, :first_response)
33
+
34
+ iterative = Benchmark.realtime { search_iteratively.search('stuff') }
35
+ threaded = Benchmark.realtime { search_threaded.search('stuff') }
36
+ first_resp = Benchmark.realtime { search_first_response.search('stuff') }
37
+
38
+ puts "Iterative Search Time: #{iterative}"
39
+ puts "Threaded Search Time : #{threaded}"
40
+ puts "First Response Time : #{first_resp}"
@@ -0,0 +1,23 @@
1
+ #container for the proxied objects
2
+ require 'group_delegator/source_group'
3
+
4
+ #retrieves the method data from the source group
5
+ require 'group_delegator/source_helper'
6
+
7
+ #class to use if one is only interested in instance methods
8
+ require 'group_delegator/group_delegator_instances'
9
+
10
+ #class to use if one is interested in proxy a complete class
11
+ #- including class methods and instantiation
12
+ require 'group_delegator/group_delegator_klasses'
13
+
14
+ #for the lazy (like me) set a group delegator class to a default
15
+ #so people don't have to read docs
16
+ class GroupDelegator < GroupDelegatorKlasses
17
+ end
18
+
19
+ #Also, here's the basic version with a simple name
20
+ #This one only deals with instantiated objects (so
21
+ #no class variables)
22
+ class SimpleGroupDelegator < GroupDelegatorInstances
23
+ end
@@ -0,0 +1,32 @@
1
+ #Takes a set of instantiated objects as arguments and will concurently delegate
2
+ #method calls to each instance in the set. Built in concurrency models include:
3
+ # - iterative (iterates the method calls on each object in the set)
4
+ # - threaded (all method calls are done in parallel until all conclude
5
+ # - first response (continues once any in the set complete the method)
6
+ class GroupDelegatorInstances
7
+ include SourceHelper
8
+ #unload these methods so the proxy object will handle them
9
+ [:to_s,:inspect,:=~,:!~,:===].each do |m|
10
+ undef_method m
11
+ end
12
+
13
+ #object methods, only
14
+ def initialize(proxied_objs, concurrency_model = :iterative)
15
+ @source_objects = [] #contains the delegated objects
16
+ @source_obj_methods = {} #map of all methods to the objects that use them
17
+ raise "No source instances set" unless proxied_objs.size > 0
18
+ sources_data = __set_sources_data(proxied_objs)
19
+ @source_obj_methods = sources_data[:source_methods]
20
+ @source_objects = sources_data[:source_objs]
21
+ @instance_source_group = SourceGroup.new(@source_objects, concurrency_model)
22
+ self
23
+ end
24
+
25
+ def method_missing(m, *args, &block)
26
+ if @source_obj_methods.include? m
27
+ resp = @instance_source_group.forward(m, *args, &block)
28
+ else
29
+ raise NoMethodError, "GroupDelegatorKlasses object can't find the method #{m} in any of its sources"
30
+ end
31
+ end
32
+ end