idregistry 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +3 -0
- data/IDRegistry.rdoc +180 -0
- data/README.rdoc +185 -0
- data/Version +1 -0
- data/lib/idregistry.rb +48 -0
- data/lib/idregistry/configuration.rb +523 -0
- data/lib/idregistry/errors.rb +66 -0
- data/lib/idregistry/middleware.rb +144 -0
- data/lib/idregistry/railtie.rb +123 -0
- data/lib/idregistry/registry.rb +580 -0
- data/lib/idregistry/utils.rb +64 -0
- data/lib/idregistry/version.rb +53 -0
- data/test/tc_categories.rb +153 -0
- data/test/tc_configuration.rb +330 -0
- data/test/tc_middleware.rb +139 -0
- data/test/tc_misc.rb +111 -0
- data/test/tc_simple_patterns.rb +221 -0
- data/test/tc_threads.rb +78 -0
- metadata +71 -0
data/History.rdoc
ADDED
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
|