idregistry 0.1.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,3 @@
1
+ === 0.1.0 / 2012-08-01
2
+
3
+ * Initial release
data/IDRegistry.rdoc ADDED
@@ -0,0 +1,180 @@
1
+ = IDRegistry
2
+
3
+ IDRegistry is a generic object generator and identity map for Ruby.
4
+
5
+ This document provides an in-depth introduction to using IDRegistry. For a quick introduction, see the README.
6
+
7
+ == Identity Maps and Registries
8
+
9
+ IDRegistry combines two patterns from Martin Fowler's book "Patterns of Enterprise Application Architecture": the Identity Map pattern and the Registry pattern.
10
+
11
+ An Identity Map is essentially an in-memory cache that references objects based on a unique identifier. Whenever you want to obtain an object, you first check the cache to see if the object already exists---technically, if the cache contains an object for the given unique identifier. If so, you pull that existing instance directly from the cache. Otherwise, you construct the object, making any necessary database calls, and then insert it into the cache. It has now effectively been memoized, and the next time you request that identifier, that same instance will be returned again from the cache.
12
+
13
+ In addition to the performance benefits of a cache, an Identity Map also ensures that at most one copy of any object will exist in your system at any time. This effectively eliminates a whole class of bugs that could arise if you construct multiple copies of an object whose states get out of sync with each other.
14
+
15
+ Identity Map is a highly useful pattern, but by itself it tends to be cumbersome to implement. This is because you have to find every point in the code that constructs a model object, and inject some code to manage the cache. To solve this, we combine it with the Registry pattern. A Registry is simply a central source for object procurement; any code that wants to obtain a model object must get it from the Registry. The Registry knows how to construct the object if needed. Thus, it is able to encapsulate the Identity Map logic, providing a one-stop solution for object procurement with caching and duplication elimination. This combination is what we call an IDRegistry.
16
+
17
+ == Tuples and Patterns
18
+
19
+ A common practice is to use database primary keys as the unique identifiers for an Identity Map. However, if you are managing multiple kinds of objects, objects that span multiple tables or aren't associated with any particular database row, or objects that otherwise need to be identified across more than one dimension, you need a more versatile unique identifier.
20
+
21
+ IDRegistry uses arrays, or <i>tuples</i> as we will call them, as unique identifiers. This allows us to support a wide variety of identification strategies. A simple example is to employ a two-element tuple: the first element being a type indicator, and the second being a database primary key. For example, suppose your application had two types of entities: users and blog posts. Your user objects could employ unique identifiers of the form <tt>[:user, <i>user-id</i>]</tt>, and your post objects could employ identifiers of the form <tt>[:post, <i>post-id</i>]</tt>. So the tuple <tt>[:user, 1]</tt> identifies the user with ID 1, and the tuple <tt>[:post, 210]</tt> identifies the post with ID 210.
22
+
23
+ Such "forms" of tuples that correspond to "types" of objects, we denote as <i>patterns</i>. A pattern is an array like a tuple, but some of its elements are placeholders rather than values. For example, <tt>[:user, 1]</tt> is a tuple that identifies the particular user with ID 1, while <tt>[:user, Integer]</tt> is the pattern followed by user identifiers. The <tt>Integer</tt> element is a placeholder that matches certain kinds of values---in this case, integer IDs. Similarly, <tt>[:post, Integer]</tt> is the pattern followed by blog post identifiers.
24
+
25
+ It is also common to use <tt>String</tt> as a placeholder element used in patterns. For example, if you have contacts that should be identified by a unique phone number, you could use tuples with the pattern <tt>[:contact, String]</tt> where the unique phone number is represented as a string.
26
+
27
+ Indeed, technically, a placeholder element can be any object that responds appropriately to the === operator. In most cases, these will be class or module objects such as <tt>Integer</tt> or <tt>String</tt> above; however, patterns can utilize regular expressions, or any other object that classifies based on the === operator.
28
+
29
+ Additionally, tuples (and patterns) can be longer and more complex than the two-element examples above. Suppose you have objects that represent relationships between users in a social network. Each relationship might be identified by the two user IDs of the users involved. Correspondingly, your identifier tuples might follow the pattern <tt>[:relationship, Integer, Integer]</tt>.
30
+
31
+ == Basic IDRegistry usage
32
+
33
+ Creating an IDRegistry is as simple as calling the <tt>create</tt> method:
34
+
35
+ my_registry = IDRegistry.create
36
+
37
+ The job of an IDRegistry is to procure objects by either returning a cached object or creating a new one. Thus, it needs to know how to construct your model objects. You accomplish this by configuring the registry at application initialization time, telling it how to construct the various types of objects it needs to manage.
38
+
39
+ In the previous section, we saw how a "type" of object can be identified with a <i>pattern</i>. Through configuration, we "teach" an IDRegistry how to construct objects for a given pattern. For example, our user objects have identifiers matching the pattern <tt>[:user, Integer]</tt>. We can configure a registry as follows:
40
+
41
+ my_registry.config.add_pattern |pattern|
42
+ pattern.pattern [:user, Integer]
43
+ pattern.to_generate_object do |tuple|
44
+ my_create_user_object_given_id(tuple[1])
45
+ end
46
+ end
47
+
48
+ The <tt>add_pattern</tt> configuration command teaches the IDRegistry about a certain pattern. If the registry encounters a tuple identifier matching that pattern, it specifies how to construct the object given that tuple.
49
+
50
+ You can add any number of pattern configurations to a registry, covering any number of object types.
51
+
52
+ Once you have configured your IDRegistry, using it is simple. Call the <tt>lookup</tt> method to obtain an object given a tuple. The registry will check its cache and return the object, or it will invoke the matching pattern configuration to create it.
53
+
54
+ user1 = my_registry.lookup(:user, 1) # constructs a user object
55
+ user1a = my_registry.lookup(:user, 1) # returns the same user object
56
+
57
+ If you want to clear the cache and force the registry to construct new objects, use the <tt>clear</tt> method:
58
+
59
+ my_registry.clear
60
+
61
+ == Multiple identifiers for an object
62
+
63
+ Sometimes there will be several different mechanisms for identifying an object to procure. Consider a tree stored in the database, in which each node has a name that is unique among its siblings. Now, each node might have a database primary key, so you could identify objects using the pattern <tt>[:node, Integer]</tt>. However, since the combination of parent and name is unique, you could also uniquely identify objects using that combination: <tt>[:node, Integer, String]</tt> for parent ID and child name. These two patterns represent two different ways of looking up a tree node:
64
+
65
+ object = find_tree_node_id(id)
66
+ object = find_tree_from_parent_id_and_child_name(parent_id, child_name)
67
+
68
+ These two ways to lookup the object correspond to two different tuples---two different unique identifiers. In a simple Identity Map, this would not work. It might create an object using the first identifier, and then create a second object using the second identifier, even if semantically they should be the same object.
69
+
70
+ IDRegistry, however, supports this case by giving you the ability to define multiple patterns and associate them with the same object type. Here's how.
71
+
72
+ my_registry.config do |config|
73
+ config.add_pattern do |pattern|
74
+ pattern.type :treenode
75
+ pattern.pattern [:node, Integer]
76
+ pattern.to_generate_object do |tuple|
77
+ find_tree_node_id(tuple[1])
78
+ end
79
+ pattern.to_generate_tuple do |obj|
80
+ [:node, obj.id]
81
+ end
82
+ end
83
+ config.add_pattern do |pattern|
84
+ pattern.type :treenode
85
+ pattern.pattern [:node, Integer, String]
86
+ pattern.to_generate_object do |tuple|
87
+ find_tree_from_parent_id_and_child_name(tuple[1], tuple[2])
88
+ end
89
+ pattern.to_generate_tuple do |obj|
90
+ [:node, obj.parent_id, obj.name]
91
+ end
92
+ end
93
+ end
94
+
95
+ Let's unpack this. First, notice that we are now specifying a "type" for each pattern. The type is a name for this type of object. If you omit it (as we did earlier), IDRegistry treats each pattern as a separate anonymous type. In this case, however, we set it explicitly. This lets us specify that both patterns describe the same type of object, a tree node object. Each tree node will now have TWO identifiers, one of each pattern. Doing a lookup for either identifiers will return the same object.
96
+
97
+ Second, now in addition to the <tt>to_generate_object</tt> block, we now provide a <tt>to_generate_tuple</tt> block. We need to tell IDRegistry how to generate a tuple (identifier) from an object. Why?
98
+
99
+ Well, suppose were were to look up a tree node by ID:
100
+
101
+ # Look up a node by database ID
102
+ node = my_registry.lookup(:node, 10)
103
+
104
+ At this point, IDRegistry can cache the object and associate it with the tuple <tt>[:node, 10]</tt> so that you can look it up using that tuple again. However, that object also has a parent and a name (suppose the parent ID is 9 and the name is "foo"). This means we would like to be able to look up that _same_ object using the tuple <tt>[:node, 9, "foo"]</tt>.
105
+
106
+ # Should the same object as the original node
107
+ node1 = my_registry.lookup(:node, node.parent_id, node.name)
108
+
109
+ IDRegistry therefore needs you to teach it how to generate that other tuple for the object. Similarly, if you originally looked up the tree node by parent ID and name, IDRegistry needs you to teach it how to generate the corresponding simple ID tuple.
110
+
111
+ So to summarize... if you have only one way to look up an object, you can simply specify the pattern and a <tt>to_generate_object</tt> block. For objects that can be looked up in more than one way, you should also include a type, to connect the various patterns together, and a <tt>to_generate_tuple</tt> block, which tells IDRegistry how to generate missing tuples.
112
+
113
+ == Categories
114
+
115
+ Identity maps generally support one-to-one correspondence between objects and unique identifiers. We have already seen how IDRegistry can support multiple identifiers for an object, for object types that require multiple modes of lookup. In this section, we cover categories, which are special identifiers that can reference zero, one, or multiple objects that are already present in the identity map. IDRegistry can look up the collection of objects that match a given category, or use categories to delete groups of objects out of the identity map quickly.
116
+
117
+ We'll cover an example. Take the "tree node" object type that we covered earlier. One of the patterns for a tree node identifies the node by parent ID and child name: <tt>[:tree, Integer, String]</tt>. This pattern lets us look up a _specific_ child of a given node. However, suppose we want _all_ the children (that have been loaded into the identity map so far) of a given parent. Somehow, we want to provide a specific parent ID, but a wildcard for the name.
118
+
119
+ That is how categories are defined. We start with an identifier pattern such as our example <tt>[:tree, Integer, String]</tt>. Now we choose some number (zero or more) of the placeholder elements that we want to _specify_. In this example, we want to specify the parent ID, so we choose array index 1, which is the parent ID in our pattern. We call this sequence of element indexes the "category indexes". In this case, the category indexes are <tt>[1]</tt>. The rest of the placeholders (in our example, array index 2, the child name) will be treated as wildcards.
120
+
121
+ To define such a category, provide a name, a pattern for identifiers, and an array of category indexes, in the registry's configuration as follows:
122
+
123
+ my_registry.config do |config|
124
+ add_category(:node_parent, [:tree, Integer, String], [1])
125
+ end
126
+
127
+ Note that what we've done here is create a _class_ of categories---specifically, the class of categories that include the children of given parent nodes. In order to specify a _particular_ category, we must say _which_ parent node. To do that, we take each of the indexes in the category index array (currently just one) and we assign each a particular value. In our example, suppose we want to specify the particular category with parent ID 9. We assign the value 9 to the first and only category index, and we get the array <tt>[9]</tt>. This new array is what we call the "category spec". It specifies a particular category out of our class, in this case, the category of tree nodes whose parent ID is 9.
128
+
129
+ To look up nodes by category, for example, you must provide the name of the class of categories, and the category spec indicating which particular category.
130
+
131
+ objects = my_registry.objects_in_category(:node_parent, [9])
132
+
133
+ There are many different ways to specify categories. Sometimes you want a single "one-off" category that isn't part of a class of categories. For example, you could define a category that includes ALL tree node objects like this:
134
+
135
+ my_registry.config do |config|
136
+ add_category(:all_nodes, [:tree, Integer, String], [])
137
+ end
138
+
139
+ Note that we set the same pattern but now we have no category indexes. This means, we are not going to specify any parameters in order to identify a particular category. Instead, both parent ID and name string are wildcards, and all tree nodes will fall into the same category. This is still a "class" of categories, but a degenerate one with no parameters. When we want to look up the objects, our category spec is similarly the empty array, since we have no category indexes for which to provide values.
140
+
141
+ objects = my_registry.objects_in_category(:all_nodes, [])
142
+
143
+ Some classes of categories may be parameterized by multiple elements. For example, if we set up a category like this:
144
+
145
+ my_registry.config do |config|
146
+ add_category(:parent_and_name, [:tree, Integer, String], [1, 2])
147
+ end
148
+
149
+ Now our category is parameterized by both parent and name. This isn't a very useful class of categories since each will contain at most one element. I show it merely to illustrate that you can have multiple category indexes. So here's how to get all the objects in the "category of objects with parent ID 9 and name foo":
150
+
151
+ objects = my_registry.objects_in_category(:parent_and_name, [9, "foo"])
152
+
153
+ Finally, here is one more example. Suppose you did this:
154
+
155
+ my_registry.config do |config|
156
+ add_category(:named_foo, [:tree, Integer, 'foo'], [])
157
+ end
158
+
159
+ Notice that we've replaced the "child name" placeholder with a particular ID. That element is no longer a placeholder, but a specific value. This is now a one-off category (i.e. a degenerate class of categories with no parameters) of objects whose name is "foo". The pattern that you use to define a category doesn't have to be exactly one of the patterns that you use to define an object type. It just has to match actual identifier tuples in your registry.
160
+
161
+ == Web requests
162
+
163
+ In a web application, you will typically want to "scope" an identity map to a request. That is, you usually won't want objects from one request leaking into the next. IDRegistry provides two different mechanisms to limit the scope of a registry.
164
+
165
+ First, if you are certain that only one request will be run at a time in any given process, you can keep a single global registry, and just clean it out at the end of a request. IDRegistry provides a Rack middleware for this purpose. If you are running Rails, you can include a Railtie that installs the middleware for you, and gives you a configuration that you can use to specify which registry to clear.
166
+
167
+ If you are running multithreaded, then you will need multiple registries, one for each request. The easiest way to do this is to configure a registry as a "template" at startup, and then use the spawn_registry method to create a new empty registry from that template for each request. A Rack middleware that implements this strategy is also provided.
168
+
169
+ == Thread safety
170
+
171
+ IDRegistry is fully thread safe. All object lookup and manipulation methods, as well as configuration methods, are re-entrant and can be called concurrently from multiple threads. The desired invariant, that a registry will contain at most one copy of any given object, will be maintained.
172
+
173
+ There is, however, one caveat that has to do with the callbacks for constructing objects or generating tuples. IDRegistry does not include those callbacks within its critical sections. (To do so would invite deadlocks.) Instead, we run callbacks eagerly and serialize later on insertion. This means it is possible, in a multi-threaded environment, for a callback to be called but its result to be discarded. Here is the scenario:
174
+
175
+ * Two threads simultaneously call Registry#lookup for the same tuple that is not already present in the identity map.
176
+ * Within IDRegistry, both threads check for the tuple and determine that it is not present.
177
+ * Both threads then call the callback to create the object. Because IDRegistry does not put any mutual exclusion on callbacks, they execute concurrently and create two separate objects.
178
+ * However, only one of the two objects will actually be used. IDRegistry will use the object created by whichever thread finishes first, and throw away the object created by the slower thread. Both threads will return the same object, the first thread's copy, from the call.
179
+
180
+ Therefore, our desired behavior still holds; however, you should be aware that it is possible for more than one copy of an object to have been created in the interim.
data/README.rdoc ADDED
@@ -0,0 +1,185 @@
1
+ = IDRegistry
2
+
3
+ IDRegistry is a generic object generator and identity map for Ruby.
4
+
5
+ == Introduction to IDRegistry
6
+
7
+ An IDRegistry is a hub for obtaining and managing domain objects in a
8
+ Ruby application. It is a configurable general registry for identifying
9
+ and constructing objects when needed, and it includes a built-in identity
10
+ map which caches objects in memory and prevents duplicates.
11
+
12
+ === An Example
13
+
14
+ That's a lot of jargon, so let's clarify with an example. Suppose you
15
+ are writing a Ruby application in which you have user objects and blog
16
+ post objects, each identified by an ID number (which might be the
17
+ primary key in a database.) In effect, you can uniquely identify any of
18
+ these domain objects by a two-element tuple (array), [type, id], where the
19
+ type is one of the symbols <tt>:user</tt> or <tt>:post</tt>, and the id
20
+ is an integer.
21
+
22
+ An IDRegistry is a central object that lets you obtain any of your
23
+ domain objects by giving it that unique identifying tuple.
24
+
25
+ # Get the user with ID 1
26
+ first_user = registry.lookup(:user, 1)
27
+
28
+ # Get the user with ID 2
29
+ second_user = registry.lookup(:user, 2)
30
+
31
+ # Get the blog post with ID 300
32
+ post = registry.lookup(:post, 300)
33
+
34
+ === Configuration
35
+
36
+ How does IDRegistry know how to construct your user and post objects?
37
+ At initialization time, you configure IDRegistry, telling it about each
38
+ type of object it will need to manage. For each object type, you provide
39
+ a pattern for the identifying tuple, and a block that constructs a new
40
+ object when needed. Here's an example:
41
+
42
+ # Create and configure the registry at initialization time
43
+ registry = Registry.create do |config|
44
+
45
+ # The pattern for identifying tuples for user objects
46
+ config.add_pattern([:user, Integer]) do |tuple|
47
+ # How to construct a user object given a tuple
48
+ my_construct_user_object_from_id(tuple[1])
49
+ end
50
+
51
+ # The pattern for identifying tuples for post objects
52
+ config.add_pattern([:post, Integer]) do |tuple|
53
+ # How to construct a post object given a tuple
54
+ my_construct_post_object_from_id(tuple[1])
55
+ end
56
+
57
+ end
58
+
59
+ Now, when you ask for a particular tuple, say, [:user, 1], the
60
+ IDRegistry finds the pattern that matches that tuple, and uses it to
61
+ construct and return the appropriate object.
62
+
63
+ === Caching
64
+
65
+ The real power, however, comes from the fact that IDRegistry now
66
+ caches all the objects it creates in an internal hash. So if you ask
67
+ for the same tuple identifier a second time, it doesn't construct a
68
+ second object, but simply returns the same object it had constructed
69
+ earlier. In other words, it has a built-in identity map.
70
+
71
+ # Get the user with ID 1
72
+ first_user = registry.lookup(:user, 1)
73
+
74
+ # If you re-ask for the same identifier, you get the same object.
75
+ same_user = registry.lookup(:user, 1)
76
+ same_user.object_id == first_user.object_id # => true
77
+
78
+ You can remove cached objects from the registry, forcing the registry
79
+ to re-construct them the next time you ask for them. A common usage
80
+ pattern in a web application is to clear out the registry cache at the
81
+ end of each request, so that each request is self-contained and has its
82
+ own copies of domain objects. A Rack middleware is provided for this
83
+ purpose.
84
+
85
+ === Tuples
86
+
87
+ Identifying tuples don't have to follow the pattern [type, id]. They
88
+ can actually be any array. For example, you might want to identify
89
+ nodes in a tree using a combination of parent and child name, rather
90
+ than just the ID. For such tree node objects, you might use a pattern
91
+ like <tt>[:tree, Node, String]</tt>, where the second element is
92
+ the parent node itself, and the third is the name of the child.
93
+
94
+ In fact, it is even possible to provide multiple ways of identifying
95
+ objects. Perhaps you want to be able to look up tree nodes by either
96
+ ID number, or by parent/child-name. In configuration, you can tell
97
+ IDRegistry that these refer to the same type of object. Then, if you
98
+ first look up an object by ID, and then later look up the same object
99
+ by parent/child-name, IDRegistry will be smart enough to know you are
100
+ referring to the same object, and will return its cached data.
101
+
102
+ === Why?
103
+
104
+ IDRegistry is an extraction of an identity map I wrote for a few large
105
+ Rails applications, including the back-end for Pirq (www.pirq.com).
106
+
107
+ Our model objects were getting quite complex with lots of associations
108
+ and dependencies, and we were having difficulty keeping track of which
109
+ objects had already been loaded, and whether we had multiple copies of
110
+ objects in memory that might be getting out of sync with one another.
111
+
112
+ After we wrote IDRegistry and refactored our object creation to use it,
113
+ our domain object management code was greatly simplified, and a whole
114
+ class of bugs was eliminated. We've been using it in production for
115
+ several years, and now we offer it to the community.
116
+
117
+ === For more info
118
+
119
+ More detailed info is available in the IDRegistry.rdoc file.
120
+
121
+ == Dependencies
122
+
123
+ IDRegistry is known to work with the following Ruby implementations:
124
+
125
+ * Standard "MRI" Ruby 1.8.7 or later. (1.9.2 or later preferred.)
126
+ * Rubinius 1.1 or later.
127
+ * JRuby 1.6 or later.
128
+
129
+ == Installation
130
+
131
+ Install IDRegistry as a gem:
132
+
133
+ gem install idregistry
134
+
135
+ == Development and support
136
+
137
+ Documentation is available at http://dazuma.github.com/idregistry/rdoc
138
+
139
+ Source code is hosted on Github at http://github.com/dazuma/idregistry
140
+
141
+ Contributions are welcome. Fork the project on Github.
142
+
143
+ Build status: {<img src="https://secure.travis-ci.org/dazuma/idregistry.png" />}[http://travis-ci.org/dazuma/idregistry]
144
+
145
+ Report bugs on Github issues at http://github.org/dazuma/idregistry/issues
146
+
147
+ Contact the author at dazuma at gmail dot com.
148
+
149
+ == Acknowledgments
150
+
151
+ IDRegistry is written by Daniel Azuma (http://www.daniel-azuma.com).
152
+
153
+ Development is supported by Pirq (http://www.pirq.com).
154
+
155
+ Continuous integration service provided by Travis-CI (http://travis-ci.org).
156
+
157
+ == License
158
+
159
+ Copyright 2012 Daniel Azuma
160
+
161
+ All rights reserved.
162
+
163
+ Redistribution and use in source and binary forms, with or without
164
+ modification, are permitted provided that the following conditions are met:
165
+
166
+ * Redistributions of source code must retain the above copyright notice,
167
+ this list of conditions and the following disclaimer.
168
+ * Redistributions in binary form must reproduce the above copyright notice,
169
+ this list of conditions and the following disclaimer in the documentation
170
+ and/or other materials provided with the distribution.
171
+ * Neither the name of the copyright holder, nor the names of any other
172
+ contributors to this software, may be used to endorse or promote products
173
+ derived from this software without specific prior written permission.
174
+
175
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
176
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
177
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
178
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
179
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
180
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
181
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
182
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
183
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
184
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
185
+ POSSIBILITY OF SUCH DAMAGE.
data/Version ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/idregistry.rb ADDED
@@ -0,0 +1,48 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # IDRegistry main file
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2012 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ # IDRegistry is a generic object generator and identity map for Ruby.
38
+
39
+ module IDRegistry
40
+ end
41
+
42
+
43
+ require 'idregistry/version'
44
+ require 'idregistry/utils'
45
+ require 'idregistry/errors'
46
+ require 'idregistry/configuration'
47
+ require 'idregistry/registry'
48
+ require 'idregistry/middleware'
@@ -0,0 +1,523 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # IDRegistry configuration object
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2012 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'blockenspiel'
38
+
39
+
40
+ module IDRegistry
41
+
42
+
43
+ # A registry configuration.
44
+ #
45
+ # Access this API by calling the configuration method of a registry.
46
+ # Conceptually, the configuration and the registry are just two
47
+ # windows (APIs) into the same object.
48
+ #
49
+ # Once objects are added to the registry, the configuration is locked
50
+ # and cannot be modified. Informational methods may still be called.
51
+
52
+ class Configuration
53
+
54
+ include ::Blockenspiel::DSL
55
+
56
+ dsl_methods false
57
+
58
+
59
+ # Object types that aren't explicitly provided will be assigned
60
+ # anonymous types that are instances of this class.
61
+
62
+ class AnonymousType; end
63
+
64
+
65
+ def initialize(registry_, patterns_, types_, categories_, methods_) # :nodoc:
66
+ @registry = registry_
67
+ @patterns = patterns_
68
+ @types = types_
69
+ @categories = categories_
70
+ @methods = methods_
71
+ @locked = false
72
+ @mutex = ::Mutex.new
73
+ end
74
+
75
+
76
+ def inspect # :nodoc:
77
+ "#<#{self.class}:0x#{object_id.to_s(16)}>"
78
+ end
79
+
80
+
81
+ # Returns true if this configuration has been locked.
82
+ # A locked configuration can no longer be modified.
83
+ # Registries lock their configurations once you start using them.
84
+
85
+ def locked?
86
+ @locked
87
+ end
88
+
89
+
90
+ # Returns the registry that owns this configuration.
91
+
92
+ def registry
93
+ @registry
94
+ end
95
+
96
+
97
+ # Create a new empty registry, duplicating this configuration.
98
+ #
99
+ # If the <tt>:unlocked</tt> option is set to true, the new registry
100
+ # will have an unlocked configuration that can be modified further.
101
+ # Otherwise, the new registry's configuration will be locked.
102
+ #
103
+ # Spawning a locked registry from a locked configuration is very fast
104
+ # because it reuses the configuration objects.
105
+
106
+ def spawn_registry(opts_={})
107
+ request_unlocked_ = opts_[:unlocked]
108
+ if @locked && !request_unlocked_
109
+ reg_ = Registry._new(@patterns, @types, @categories, @methods)
110
+ reg_.config.lock
111
+ else
112
+ patterns_ = {}
113
+ types_ = {}
114
+ categories_ = {}
115
+ methods_ = {}
116
+ @mutex.synchronize do
117
+ @patterns.each{ |k_, v_| patterns_[k_] = v_.dup }
118
+ @types.each{ |k_, v_| types_[k_] = v_.dup }
119
+ @categories.each{ |k_, v_| categories_[k_] = v_.dup }
120
+ @methods.each{ |k_, v_| methods_[k_] = v_.dup }
121
+ end
122
+ reg_ = Registry._new(patterns_, types_, categories_, methods_)
123
+ reg_.config.lock unless request_unlocked_
124
+ end
125
+ reg_
126
+ end
127
+
128
+
129
+ dsl_methods true
130
+
131
+
132
+ # Lock the configuration, preventing further changes.
133
+ #
134
+ # This is called by registries when you start using them.
135
+ #
136
+ # In addition, it is cheap to spawn another registry from a
137
+ # configuration that is locked, because the configuration internals
138
+ # can be reused. Therefore, you should lock a configuration if you
139
+ # want to use it as a template to create empty registries quickly
140
+ # (using the spawn_registry call).
141
+
142
+ def lock
143
+ @mutex.synchronize do
144
+ @locked = true
145
+ end
146
+ self
147
+ end
148
+
149
+
150
+ # Returns an array of all patterns known by this configuration.
151
+ #
152
+ # The pattern arrays will be duplicates of the actual arrays
153
+ # stored internally, so you cannot modify patterns in place.
154
+
155
+ def all_patterns
156
+ @mutex.synchronize do
157
+ @patterns.keys.map{ |a_| a_.dup }
158
+ end
159
+ end
160
+
161
+
162
+ # Returns an array of all object types known by this configuration.
163
+ #
164
+ # Does not include any "anonymous" types that are automatically
165
+ # generated if you add a pattern without a type.
166
+
167
+ def all_types
168
+ @mutex.synchronize do
169
+ @types.keys.find_all{ |t_| !t_.is_a?(AnonymousType) }
170
+ end
171
+ end
172
+
173
+
174
+ # Returns an array of all category types known by this configuration.
175
+
176
+ def all_categories
177
+ @mutex.synchronize do
178
+ @categories.keys
179
+ end
180
+ end
181
+
182
+
183
+ # Returns an array of all convenience method names known by this
184
+ # configuration.
185
+
186
+ def all_convenience_methods
187
+ @mutex.synchronize do
188
+ @methods.keys
189
+ end
190
+ end
191
+
192
+
193
+ # Returns true if this configuration includes the given pattern.
194
+
195
+ def has_pattern?(pattern_)
196
+ @mutex.synchronize do
197
+ @patterns.has_key?(pattern_)
198
+ end
199
+ end
200
+
201
+
202
+ # Returns true if this configuration includes the given object type.
203
+
204
+ def has_type?(type_)
205
+ @mutex.synchronize do
206
+ @types.has_key?(type_)
207
+ end
208
+ end
209
+
210
+
211
+ # Returns true if this configuration includes the given category type.
212
+
213
+ def has_category?(category_)
214
+ @mutex.synchronize do
215
+ @categories.has_key?(category_)
216
+ end
217
+ end
218
+
219
+
220
+ # Returns true if this configuration includes the given convenience method.
221
+
222
+ def has_convenience_method?(method_)
223
+ @mutex.synchronize do
224
+ @methods.has_key?(method_)
225
+ end
226
+ end
227
+
228
+
229
+ # Returns the object type corresponding to the given pattern.
230
+ # Returns nil if the given pattern is not recognized.
231
+
232
+ def type_for_pattern(pattern_)
233
+ @mutex.synchronize do
234
+ patdata_ = @patterns[pattern_]
235
+ patdata_ ? patdata_[0] : nil
236
+ end
237
+ end
238
+
239
+
240
+ # Returns an array of patterns corresponding to the given object type.
241
+ # Returns the empty array if the given object type is not recognized.
242
+
243
+ def patterns_for_type(type_)
244
+ @mutex.synchronize do
245
+ typedata_ = @types[type_]
246
+ typedata_ ? typedata_.dup : []
247
+ end
248
+ end
249
+
250
+
251
+ # Add a pattern to the configuration.
252
+ #
253
+ # You may use one of the following call sequences:
254
+ #
255
+ # [<tt>add_pattern( <i>pattern</i> ) { ... }</tt>]
256
+ # Add a simple pattern, using the given block to generate objects
257
+ # matching that pattern.
258
+ #
259
+ # [<tt>add_pattern( <i>pattern</i>, <i>to_generate_object</i> )</tt>]
260
+ # Add a simple pattern, using the given proc to generate objects
261
+ # matching that pattern.
262
+ #
263
+ # [<tt>add_pattern( <i>pattern</i>, <i>to_generate_object</i>, <i>to_generate_tuple</i> )</tt>]
264
+ # Add a simple pattern, using the given proc to generate objects
265
+ # matching that pattern, and to generate a tuple from an object.
266
+ #
267
+ # [<tt>add_pattern( <i>pattern</i>, <i>type</i>, <i>to_generate_object</i>, <i>to_generate_tuple</i> )</tt>]
268
+ # Add a pattern for the given type. You should provide both a proc
269
+ # to generate objects, and a proc to generate a tuple from an object.
270
+ #
271
+ # [<tt>add_pattern() { ... }</tt>]
272
+ # Utilize a PatternAdder DSL to define the pattern.
273
+
274
+ def add_pattern(*args_, &block_)
275
+ raise ConfigurationLockedError if @locked
276
+ if block_
277
+ case args_.size
278
+ when 0
279
+ adder_ = PatternAdder._new(nil, nil, nil, nil)
280
+ ::Blockenspiel.invoke(block_, adder_)
281
+ when 1
282
+ adder_ = PatternAdder._new(args_[0], nil, block_, nil)
283
+ else
284
+ raise IllegalConfigurationError, "Did not recognize call sequence for add_pattern"
285
+ end
286
+ else
287
+ case args_.size
288
+ when 2, 3
289
+ adder_ = PatternAdder._new(args_[0], nil, args_[1], args_[2])
290
+ when 4
291
+ adder_ = PatternAdder._new(args_[0], args_[1], args_[2], args_[3])
292
+ else
293
+ raise IllegalConfigurationError, "Did not recognize call sequence for add_pattern"
294
+ end
295
+ end
296
+ pattern_ = adder_.pattern
297
+ type_ = adder_.type || AnonymousType.new
298
+ gen_obj_ = adder_.to_generate_object
299
+ gen_tuple_ = adder_.to_generate_tuple
300
+ @mutex.synchronize do
301
+ raise ConfigurationLockedError if @locked
302
+ if @patterns.has_key?(pattern_)
303
+ raise IllegalConfigurationError, "Pattern already exists"
304
+ end
305
+ @patterns[pattern_] = [type_, gen_obj_, gen_tuple_]
306
+ (@types[type_] ||= []) << pattern_
307
+ end
308
+ self
309
+ end
310
+
311
+
312
+ # Remove the given pattern from this configuration.
313
+ # Automatically removes the object type if this is the object type's
314
+ # only remaining pattern.
315
+
316
+ def delete_pattern(pattern_)
317
+ @mutex.synchronize do
318
+ raise ConfigurationLockedError if @locked
319
+ if (patdata_ = @patterns.delete(pattern_))
320
+ type_ = patdata_[0]
321
+ typedata_ = @types[type_]
322
+ typedata_.delete(pattern_)
323
+ @types.delete(type_) if typedata_.empty?
324
+ end
325
+ end
326
+ self
327
+ end
328
+
329
+
330
+ # Remove the given object type from this configuration.
331
+ # Automatically removes all patterns associated with this object type.
332
+
333
+ def delete_type(type_)
334
+ @mutex.synchronize do
335
+ raise ConfigurationLockedError if @locked
336
+ if (typedata_ = @types.delete(type_))
337
+ typedata_.each{ |pat_| @patterns.delete(pat_) }
338
+ end
339
+ end
340
+ self
341
+ end
342
+
343
+
344
+ # Add a category type.
345
+ #
346
+ # You must provide a category type name, a pattern that recognizes
347
+ # tuples that should trigger this category, and an array of indexes
348
+ # into the pattern that indicates which tuple element(s) will
349
+ # identify individual categories within this category type.
350
+
351
+ def add_category(category_, pattern_, indexes_=[])
352
+ @mutex.synchronize do
353
+ raise ConfigurationLockedError if @locked
354
+ if @categories.has_key?(category_)
355
+ raise IllegalConfigurationError, "Category already exists"
356
+ end
357
+ @categories[category_] = [pattern_, indexes_]
358
+ end
359
+ self
360
+ end
361
+
362
+
363
+ # Remove a category type by name.
364
+
365
+ def delete_category(category_)
366
+ @mutex.synchronize do
367
+ raise ConfigurationLockedError if @locked
368
+ @categories.delete(category_)
369
+ end
370
+ self
371
+ end
372
+
373
+
374
+ # Add a convenience method, providing a short cut for doing lookups
375
+ # in the registry. You must provide a pattern that serves as a tuple
376
+ # template, and an array of indexes. The method will take a number of
377
+ # arguments corresponding to that array, and the indexes will then be
378
+ # used as indexes into the pattern, replacing pattern elements to
379
+ # generate the actual tuple to be looked up.
380
+
381
+ def add_convenience_method(name_, pattern_, indexes_)
382
+ @mutex.synchronize do
383
+ raise ConfigurationLockedError if @locked
384
+ name_ = name_.to_sym
385
+ if @methods.has_key?(name_)
386
+ raise IllegalConfigurationError, "Factory method already exists"
387
+ end
388
+ @methods[name_] = [pattern_, indexes_]
389
+ end
390
+ self
391
+ end
392
+
393
+
394
+ # Delete a convenience method by name.
395
+
396
+ def delete_convenience_method(name_)
397
+ @mutex.synchronize do
398
+ raise ConfigurationLockedError if @locked
399
+ @methods.delete(name_.to_sym)
400
+ end
401
+ self
402
+ end
403
+
404
+
405
+ # Clear all configuration information, including all object types,
406
+ # patterns, categories, and convenience methods.
407
+
408
+ def clear
409
+ @mutex.synchronize do
410
+ raise ConfigurationLockedError if @locked
411
+ @patterns.clear
412
+ @types.clear
413
+ @categories.clear
414
+ @methods.clear
415
+ end
416
+ self
417
+ end
418
+
419
+
420
+ class << self
421
+
422
+ # :stopdoc:
423
+ alias_method :_new, :new
424
+ private :new
425
+ # :startdoc:
426
+
427
+ end
428
+
429
+
430
+ end
431
+
432
+
433
+ # This is the DSL available within the block passed to
434
+ # Configuration#add_pattern.
435
+
436
+ class PatternAdder
437
+
438
+ include ::Blockenspiel::DSL
439
+
440
+ def initialize(pattern_, type_, gen_obj_, gen_tuple_) # :nodoc:
441
+ @pattern = pattern_
442
+ @type = type_
443
+ @gen_obj = gen_obj_
444
+ @gen_tuple = gen_tuple_
445
+ end
446
+
447
+
448
+ def inspect # :nodoc:
449
+ "#<#{self.class}:0x#{object_id.to_s(16)}>"
450
+ end
451
+
452
+
453
+ # Set the pattern to add
454
+
455
+ def pattern(value_=nil)
456
+ if value_
457
+ @pattern = value_
458
+ else
459
+ @pattern
460
+ end
461
+ end
462
+
463
+
464
+ # Set the object type
465
+
466
+ def type(value_=nil)
467
+ if value_
468
+ @type = value_
469
+ else
470
+ @type
471
+ end
472
+ end
473
+
474
+
475
+ # Provide a block to call to generate the appropriate object given a
476
+ # tuple. This block is called when the repository is asked to lookup
477
+ # an object given a tuple, and the provided tuple is not yet present.
478
+ #
479
+ # The block may take up to three arguments.
480
+ # The first is the tuple.
481
+ # The second is the repository containing the object.
482
+ # The third is a hash of arguments passed to the repository's lookup
483
+ # method.
484
+ #
485
+ # The block should return the generated object, or nil if it is unable
486
+ # to generate an object for the given tuple.
487
+
488
+ def to_generate_object(&block_)
489
+ if block_
490
+ @gen_obj = block_
491
+ else
492
+ @gen_obj
493
+ end
494
+ end
495
+
496
+
497
+ # Provide a block to call to generate the tuple corresponding to an
498
+ # object. The repository calls this block when an object is added, in
499
+ # order to generate the appropriate tuples for looking up the object.
500
+
501
+ def to_generate_tuple(&block_)
502
+ if block_
503
+ @gen_tuple = block_
504
+ else
505
+ @gen_tuple
506
+ end
507
+ end
508
+
509
+
510
+ class << self
511
+
512
+ # :stopdoc:
513
+ alias_method :_new, :new
514
+ private :new
515
+ # :startdoc:
516
+
517
+ end
518
+
519
+
520
+ end
521
+
522
+
523
+ end