poro 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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.configure_for_klass(klass)
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
@@ -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
- self.set_primary_key_value(obj, obj.object_id) if(pk_id.nil?)
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[obj.id] = convert_to_data(obj)
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(obj.id)
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
- # 1. When matching the last key in the list, we can stop processing when
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
- data = limit( filter( sort( data_store.dup, opts[:order] ), opts[:conditions] ), opts[:limit])
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( !self.encode_symbols && obj.kind_of?(Symbol) )
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(value, :embedded => true)
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?['values'] )
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)
@@ -3,5 +3,5 @@
3
3
  # existing plain ol' ruby objects as little as possible. For more information
4
4
  # see README.rdoc.
5
5
  module Poro
6
- VERSION = '0.1.1'
6
+ VERSION = '0.1.2'
7
7
  end
@@ -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 = {'id' => 3, 'first_name' => 'Bridgette', 'last_name' => 'Smith'}
90
- karen_zeta = {:id => 4, :first_name => 'Karen', :last_name => 'Zeta', :friends => [george_archer, george_smith]}
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
- - 1
9
- version: 0.1.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-24 00:00:00 -07:00
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