poro 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +4 -0
- data/lib/poro/context.rb +6 -1
- data/lib/poro/context_factories.rb +2 -1
- data/lib/poro/context_factories/namespace_factory.rb +118 -0
- data/lib/poro/context_factory.rb +6 -1
- data/lib/poro/contexts/hash_context.rb +32 -11
- data/lib/poro/contexts/mongo_context.rb +4 -4
- data/lib/poro/version.rb +1 -1
- data/spec/hash_context_spec.rb +8 -7
- data/spec/namespace_context_factory_spec.rb +94 -0
- metadata +5 -3
data/README.rdoc
CHANGED
@@ -160,6 +160,10 @@ mailto:jeff@paploo.net
|
|
160
160
|
|
161
161
|
= Version History
|
162
162
|
|
163
|
+
[0.1.2 - 2010-Sep-30] Feature Additions
|
164
|
+
* Added a module namespace factory.
|
165
|
+
* HashContext: Find one is faster when the conditions restrict on the primary key.
|
166
|
+
* Many HashContext bugs fixed.
|
163
167
|
[0.1.1 - 2010-Sep-24] Minor Additions and Bug Fixes.
|
164
168
|
* MongoContext now can optionally encode Symbols as hashes
|
165
169
|
or just leave them as strings.
|
data/lib/poro/context.rb
CHANGED
@@ -37,7 +37,7 @@ module Poro
|
|
37
37
|
#
|
38
38
|
# This really just fetches (and creates, if necessary) the
|
39
39
|
# Context for the class, and then yields it to the block. Returns the context.
|
40
|
-
def self.
|
40
|
+
def self.configure_for_class(klass)
|
41
41
|
context = self.fetch(klass)
|
42
42
|
yield(context) if block_given?
|
43
43
|
return context
|
@@ -53,6 +53,11 @@ module Poro
|
|
53
53
|
ContextFactory.instance = context_factory
|
54
54
|
end
|
55
55
|
|
56
|
+
# Returns true if there is a factory assigned to the application.
|
57
|
+
def self.factory?
|
58
|
+
ContextFactory.has_instance?
|
59
|
+
end
|
60
|
+
|
56
61
|
# Initizialize this context for the given class. Yields self if a block
|
57
62
|
# is given, so that instances can be easily configured at instantiation.
|
58
63
|
#
|
@@ -1 +1,2 @@
|
|
1
|
-
require 'poro/context_factories/single_store'
|
1
|
+
require 'poro/context_factories/single_store'
|
2
|
+
require 'poro/context_factories/namespace_factory'
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Poro
|
2
|
+
module ContextFactories
|
3
|
+
# The NamespaceFactory uses the module namespace of the class to determine
|
4
|
+
# which factory should be used to create a class instance.
|
5
|
+
#
|
6
|
+
# On initialization, the default factory should be set. Until other
|
7
|
+
# factories are registerd, all classes will recieve contexts created by
|
8
|
+
# this factory.
|
9
|
+
#
|
10
|
+
# Other factories can be registered against various namespaces. Namespaces
|
11
|
+
# are given as a string and are evaluated from the bottom up. For example,
|
12
|
+
# if a factory is registered against the class Foo::Bar, then Foo::Bar and
|
13
|
+
# Foo::Bar::Baz would each recieve contexts created via that factory, while
|
14
|
+
# contexts registered against Foo::Zap would use the default context.
|
15
|
+
class NamespaceFactory < ContextFactory
|
16
|
+
|
17
|
+
# Initialize this namespace factory instance with the given default
|
18
|
+
# factory. Use <tt>register_factory</tt> to add more kinds of factories
|
19
|
+
# to this app.
|
20
|
+
#
|
21
|
+
# If given a block, it is yielded self for further configuration. This
|
22
|
+
# departs slightly from the standard of yielding a Class or
|
23
|
+
# Class and Context for configuration, but as this is more useful for
|
24
|
+
# practicle applications. (It also doesn't configure a context itself,
|
25
|
+
# so giving separate blocks to the sub-factories given to it is more
|
26
|
+
# appropriate.
|
27
|
+
def initialize(default_factory=nil)
|
28
|
+
@root_node = CacheNode.new
|
29
|
+
@root_node.factory = default_factory
|
30
|
+
|
31
|
+
yield(self) if block_given?
|
32
|
+
|
33
|
+
super() do |klass|
|
34
|
+
factory = self.fetch_factory(klass)
|
35
|
+
factory.nil? ? nil : factory.fetch(klass)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Registers a factory for a given namespace, given by a string. If a
|
40
|
+
# factory is already registered for this namespace, it overrides it.
|
41
|
+
#
|
42
|
+
# This registers the given factory not only for this namespace, but for
|
43
|
+
# any sub-namespaces that haven't been specifically overriden. Sub-namespace
|
44
|
+
# overrides can be registered either before or after the namespace it overrides.
|
45
|
+
def register_factory(factory, namespace='')
|
46
|
+
namespace_stack = namespace.to_s.split('::')
|
47
|
+
|
48
|
+
parse_block = lambda do |factory, namespace_stack, node|
|
49
|
+
if( namespace_stack.length == 0 )
|
50
|
+
node.factory = factory
|
51
|
+
else
|
52
|
+
name, *rest = namespace_stack
|
53
|
+
child_node = node.children[name] || CacheNode.new
|
54
|
+
node.children[name] = child_node
|
55
|
+
parse_block.call(factory, rest, child_node)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
parse_block.call(factory, namespace_stack, @root_node)
|
60
|
+
|
61
|
+
return factory
|
62
|
+
end
|
63
|
+
|
64
|
+
# Fetches the factory for a given namespace.
|
65
|
+
#
|
66
|
+
# This grabs the factory for the most specific matching namespace.
|
67
|
+
def fetch_factory(namespace='')
|
68
|
+
namespace_stack = namespace.to_s.split('::')
|
69
|
+
|
70
|
+
lookup_block = lambda do |namespace_stack, last_seen_factory, node|
|
71
|
+
last_seen_factory = node.factory || last_seen_factory
|
72
|
+
if( namespace_stack.length == 0 )
|
73
|
+
last_seen_factory
|
74
|
+
elsif( !(node.children.include?(namespace_stack[0])) )
|
75
|
+
last_seen_factory
|
76
|
+
else
|
77
|
+
name, *rest = namespace_stack
|
78
|
+
lookup_block.call(rest, last_seen_factory, node.children[name])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
return lookup_block.call(namespace_stack, nil, @root_node)
|
83
|
+
end
|
84
|
+
|
85
|
+
# The internal class used to manage the namespace tree. This should
|
86
|
+
# never be used outside of this factory.
|
87
|
+
#
|
88
|
+
# This class stores the child notes for the namespace, as well as the
|
89
|
+
# factory for this level in the namespace.
|
90
|
+
class CacheNode
|
91
|
+
|
92
|
+
# Initialize an empty node.
|
93
|
+
def initialize
|
94
|
+
@children = {}
|
95
|
+
@factory = nil
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the children--as a hash. The keys are the module/class name,
|
99
|
+
# and the values are the associated child node.
|
100
|
+
#
|
101
|
+
# This is the raw hash and can be manipulated directly.
|
102
|
+
attr_reader :children
|
103
|
+
|
104
|
+
# Returns the factory for this node, or nil if there is none.
|
105
|
+
attr_reader :factory
|
106
|
+
|
107
|
+
# Sets the factory for this node.
|
108
|
+
attr_writer :factory
|
109
|
+
|
110
|
+
def to_s
|
111
|
+
return {:factory => factory, :children => children}.inspect
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/poro/context_factory.rb
CHANGED
@@ -23,6 +23,11 @@ module Poro
|
|
23
23
|
@instance = instance
|
24
24
|
end
|
25
25
|
|
26
|
+
# Returns true if a context factory instance is configured.
|
27
|
+
def self.has_instance?
|
28
|
+
return (@instance != nil)
|
29
|
+
end
|
30
|
+
|
26
31
|
# Takes a factory block that delivers a configured context for the class
|
27
32
|
# passed to it.
|
28
33
|
def initialize(&context_factory_block)
|
@@ -31,7 +36,7 @@ module Poro
|
|
31
36
|
end
|
32
37
|
|
33
38
|
def context_managed_class?(klass)
|
34
|
-
return klass.include?(Poro::Persistify)
|
39
|
+
return klass && klass.include?(Poro::Persistify)
|
35
40
|
end
|
36
41
|
|
37
42
|
# Fetches the context for a given class, or returns nil if the given object
|
@@ -18,9 +18,12 @@ module Poro
|
|
18
18
|
# Save the object in the underlying hash, using the object id as the key.
|
19
19
|
def save(obj)
|
20
20
|
pk_id = self.primary_key_value(obj)
|
21
|
-
|
21
|
+
if(pk_id.nil?)
|
22
|
+
pk_id = obj.object_id
|
23
|
+
self.set_primary_key_value(obj, pk_id)
|
24
|
+
end
|
22
25
|
|
23
|
-
data_store[
|
26
|
+
data_store[pk_id] = convert_to_data(obj)
|
24
27
|
return self
|
25
28
|
end
|
26
29
|
|
@@ -28,7 +31,7 @@ module Poro
|
|
28
31
|
def remove(obj)
|
29
32
|
pk_id = self.primary_key_value(obj)
|
30
33
|
if( pk_id != nil )
|
31
|
-
data_store.delete(
|
34
|
+
data_store.delete(pk_id)
|
32
35
|
self.set_primary_key_value(obj, nil)
|
33
36
|
end
|
34
37
|
return self
|
@@ -44,24 +47,42 @@ module Poro
|
|
44
47
|
|
45
48
|
private
|
46
49
|
|
47
|
-
def clean_id(id)
|
48
|
-
return id && id.to_i
|
49
|
-
end
|
50
|
-
|
51
50
|
# Searching a hash is incredibly slow because the following steps must
|
52
51
|
# be taken:
|
53
52
|
# 1. If there is an order, we first have to sort ALL values by the order.
|
54
53
|
# 2. Then we must find all matching records.
|
55
54
|
# 3. Then we must apply limit and offset to fetch the correct record.
|
56
55
|
#
|
56
|
+
# There are several optimizations to this that have already been done:
|
57
|
+
# * If the conditions include the primary key, use fetch and drop that
|
58
|
+
# condition.
|
59
|
+
# * If the offset is higher than the total number of stored records, then
|
60
|
+
# we know there will be no matches.
|
61
|
+
#
|
57
62
|
# There are several optimizations that can be made in the future:
|
58
|
-
#
|
63
|
+
# * When matching the last key in the list, we can stop processing when
|
59
64
|
# we reach the limit+offset number of records.
|
60
|
-
# 2. If the offset is higher than the total number of stored records, then
|
61
|
-
# we know there will be no matches.
|
62
65
|
def find_all(opts)
|
63
66
|
opts = clean_find_opts(opts)
|
64
|
-
|
67
|
+
|
68
|
+
# If the offset is bigger than the stored number of records, we know that
|
69
|
+
# we'll get nothing:
|
70
|
+
return [] if( (opts[:limit]&&opts[:limit][:offset]).to_i > data_store.length )
|
71
|
+
|
72
|
+
# If a search condition is the primary key, we can significantly limit our work.
|
73
|
+
values = nil
|
74
|
+
data = nil
|
75
|
+
if( opts[:conditions].has_key?( self.primary_key ) )
|
76
|
+
pk_value = opts[:conditions].delete(self.primary_key)
|
77
|
+
obj = self.fetch( pk_value )
|
78
|
+
values = obj.nil? ? [] : [obj]
|
79
|
+
data = limit( filter( values, opts[:conditions]), opts[:limit] )
|
80
|
+
else
|
81
|
+
values = data_store.values
|
82
|
+
data = limit( filter( sort( values, opts[:order] ), opts[:conditions] ), opts[:limit])
|
83
|
+
end
|
84
|
+
|
85
|
+
# Now do the search.
|
65
86
|
return data.map {|data| convert_to_plain_object(data)}
|
66
87
|
end
|
67
88
|
|
@@ -149,7 +149,7 @@ module Poro
|
|
149
149
|
obj.kind_of?(Float) ||
|
150
150
|
obj.kind_of?(String) ||
|
151
151
|
obj.kind_of?(Time) ||
|
152
|
-
(self.encode_symbols && obj.kind_of?(Symbol)) ||
|
152
|
+
(!self.encode_symbols && obj.kind_of?(Symbol)) ||
|
153
153
|
obj==true ||
|
154
154
|
obj==false ||
|
155
155
|
obj.nil? ||
|
@@ -207,7 +207,7 @@ module Poro
|
|
207
207
|
return encode_class(obj)
|
208
208
|
elsif( obj.kind_of?(Set) )
|
209
209
|
return encode_set(obj)
|
210
|
-
elsif(
|
210
|
+
elsif( self.encode_symbols && obj.kind_of?(Symbol) )
|
211
211
|
return encode_symbol(obj)
|
212
212
|
elsif( Context.managed_class?(obj.class) && Context.fetch(obj.class).kind_of?(self.class) )
|
213
213
|
return encode_mongo_managed_object(obj)
|
@@ -223,7 +223,7 @@ module Poro
|
|
223
223
|
# Recursively encode a hash's contents.
|
224
224
|
def encode_hash(hash)
|
225
225
|
return hash.inject({}) do |hash,(k,v)|
|
226
|
-
hash[k] = self.convert_to_data(
|
226
|
+
hash[k] = self.convert_to_data(v, :embedded => true)
|
227
227
|
hash
|
228
228
|
end
|
229
229
|
end
|
@@ -387,7 +387,7 @@ module Poro
|
|
387
387
|
# Decode the set depending on if it was encoded as an array or as a raw
|
388
388
|
# object.
|
389
389
|
def decode_set(set_data)
|
390
|
-
if( set_data.include?
|
390
|
+
if( set_data.include?('values') )
|
391
391
|
return Set.new(set_data['values'])
|
392
392
|
else
|
393
393
|
return decode_unmanaged_object(set_data)
|
data/lib/poro/version.rb
CHANGED
data/spec/hash_context_spec.rb
CHANGED
@@ -76,18 +76,21 @@ describe "HashContext" do
|
|
76
76
|
|
77
77
|
class HashContextPerson
|
78
78
|
include Poro::Persistify
|
79
|
-
def initialize(id, first_name, last_name, friends)
|
79
|
+
def initialize(id, first_name, last_name, friends=[])
|
80
80
|
@id = id
|
81
81
|
@first_name = first_name
|
82
82
|
@last_name = last_name
|
83
83
|
@friends = friends
|
84
84
|
end
|
85
|
+
|
86
|
+
attr_reader :id
|
87
|
+
attr_writer :id
|
85
88
|
end
|
86
89
|
|
87
|
-
george_smith = HashContextPerson.new(1, 'George', 'Smith'
|
90
|
+
george_smith = HashContextPerson.new(1, 'George', 'Smith')
|
88
91
|
george_archer = HashContextPerson.new(2, 'George', 'Archer', [george_smith])
|
89
|
-
bridgette_smith =
|
90
|
-
karen_zeta =
|
92
|
+
bridgette_smith = HashContextPerson.new(3, 'Bridgette', 'Smith')
|
93
|
+
karen_zeta = HashContextPerson.new(4, 'Karen', 'Zeta', [george_archer, george_smith])
|
91
94
|
@data = [
|
92
95
|
george_smith,
|
93
96
|
george_archer,
|
@@ -96,9 +99,7 @@ describe "HashContext" do
|
|
96
99
|
]
|
97
100
|
|
98
101
|
@context = Poro::Contexts::HashContext.new(HashContextPerson)
|
99
|
-
|
100
|
-
# Cheat to jam the data in there:
|
101
|
-
@context.data_store = @data
|
102
|
+
@data.each {|person| @context.save(person)}
|
102
103
|
end
|
103
104
|
|
104
105
|
it 'should get shallow values' do
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "Namespace Context Factory" do
|
4
|
+
|
5
|
+
it 'should register and fetch a default factory' do
|
6
|
+
default_factory = :default_factory
|
7
|
+
|
8
|
+
factory = Poro::ContextFactories::NamespaceFactory.new(default_factory)
|
9
|
+
factory.fetch_factory.should == default_factory
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should register and fetch a namespaced factory' do
|
13
|
+
default_factory = :default_factory
|
14
|
+
foo_factory = :foo_factory
|
15
|
+
|
16
|
+
factory = Poro::ContextFactories::NamespaceFactory.new(default_factory)
|
17
|
+
factory.register_factory(foo_factory, "Foo")
|
18
|
+
|
19
|
+
factory.fetch_factory.should == default_factory
|
20
|
+
factory.fetch_factory("Foo").should == foo_factory
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should get the nearest matching factory' do
|
24
|
+
default_factory = :default_factory
|
25
|
+
foo_factory = :foo_factory
|
26
|
+
bar_factory = :bar_factory
|
27
|
+
chocolate_factory = :chocolate_factory
|
28
|
+
|
29
|
+
factory = Poro::ContextFactories::NamespaceFactory.new(default_factory)
|
30
|
+
factory.register_factory(foo_factory, "Foo")
|
31
|
+
factory.register_factory(bar_factory, "Foo::Bar")
|
32
|
+
factory.register_factory(chocolate_factory, "Candy::Chocolate")
|
33
|
+
|
34
|
+
factory.fetch_factory.should == default_factory
|
35
|
+
|
36
|
+
factory.fetch_factory("Foo").should == foo_factory
|
37
|
+
factory.fetch_factory("Foo::Bar").should == bar_factory
|
38
|
+
factory.fetch_factory("Foo::Bar::Baz").should == bar_factory
|
39
|
+
factory.fetch_factory("Foo::Fiz").should == foo_factory
|
40
|
+
|
41
|
+
factory.fetch_factory("Chocolate").should == default_factory
|
42
|
+
factory.fetch_factory("Candy::Chocolate").should == chocolate_factory
|
43
|
+
factory.fetch_factory("Candy::Chocolate::Dark").should == chocolate_factory
|
44
|
+
factory.fetch_factory("Candy").should == default_factory
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should fetch a context' do
|
48
|
+
@klass_1 = Class.new(Object)
|
49
|
+
@klass_1.send(:include, Poro::Persistify)
|
50
|
+
@klass_2 = Class.new(Object)
|
51
|
+
@klass_2.send(:include, Poro::Persistify)
|
52
|
+
::PoroNamespacedContextFactoryTestFetchOne = @klass_1
|
53
|
+
::PoroNamespacedContextFactoryTestFetcgTwo = @klass_2
|
54
|
+
|
55
|
+
@klass_1.name.should_not == @klass_2.name
|
56
|
+
|
57
|
+
default_factory = Poro::ContextFactory.new {|klass| "default #{klass.name}"}
|
58
|
+
foo_factory = Poro::ContextFactory.new {|klass| "foo #{klass.name}"}
|
59
|
+
|
60
|
+
factory = Poro::ContextFactories::NamespaceFactory.new(default_factory)
|
61
|
+
factory.register_factory(foo_factory, @klass_2.name)
|
62
|
+
|
63
|
+
context = factory.fetch(@klass_1)
|
64
|
+
context.should == "default #{@klass_1.name}"
|
65
|
+
|
66
|
+
context = factory.fetch(@klass_2)
|
67
|
+
context.should == "foo #{@klass_2.name}"
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should take a configuration block' do
|
71
|
+
@klass_1 = Class.new(Object)
|
72
|
+
@klass_1.send(:include, Poro::Persistify)
|
73
|
+
@klass_2 = Class.new(Object)
|
74
|
+
@klass_2.send(:include, Poro::Persistify)
|
75
|
+
::PoroNamespacedContextFactoryTestBlockOne = @klass_1
|
76
|
+
::PoroNamespacedContextFactoryTestBlockTwo = @klass_2
|
77
|
+
|
78
|
+
@klass_1.name.should_not == @klass_2.name
|
79
|
+
|
80
|
+
default_factory = Poro::ContextFactory.new {|klass| "default #{klass.name}"}
|
81
|
+
foo_factory = Poro::ContextFactory.new {|klass| "foo #{klass.name}"}
|
82
|
+
|
83
|
+
factory = Poro::ContextFactories::NamespaceFactory.new(default_factory) do |f|
|
84
|
+
f.register_factory(foo_factory, @klass_2.name)
|
85
|
+
end
|
86
|
+
|
87
|
+
context = factory.fetch(@klass_1)
|
88
|
+
context.should == "default #{@klass_1.name}"
|
89
|
+
|
90
|
+
context = factory.fetch(@klass_2)
|
91
|
+
context.should == "foo #{@klass_2.name}"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jeff Reinecke
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-09-
|
17
|
+
date: 2010-09-30 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
@@ -31,6 +31,7 @@ files:
|
|
31
31
|
- LICENSE.txt
|
32
32
|
- Rakefile
|
33
33
|
- lib/poro/context.rb
|
34
|
+
- lib/poro/context_factories/namespace_factory.rb
|
34
35
|
- lib/poro/context_factories/README.txt
|
35
36
|
- lib/poro/context_factories/single_store/hash_factory.rb
|
36
37
|
- lib/poro/context_factories/single_store/mongo_factory.rb
|
@@ -56,6 +57,7 @@ files:
|
|
56
57
|
- spec/modelfy.rb
|
57
58
|
- spec/module_finder_spec.rb
|
58
59
|
- spec/mongo_context_spec.rb
|
60
|
+
- spec/namespace_context_factory_spec.rb
|
59
61
|
- spec/single_store_spec.rb
|
60
62
|
- spec/spec_helper.rb
|
61
63
|
has_rdoc: true
|