ninjudd-active_document 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 2
4
+ :major: 0
@@ -0,0 +1,171 @@
1
+ class ActiveDocument::Base
2
+ def self.path(path = nil)
3
+ if path
4
+ @path = path
5
+ else
6
+ @path || (base_class? ? DEFAULT_PATH : super)
7
+ end
8
+ end
9
+
10
+ def self.database_name(database_name = nil)
11
+ if database_name
12
+ raise 'cannot modify database_name after db has been initialized' if @database_name
13
+ @database_name = database_name
14
+ else
15
+ return nil if self == ActiveDocument::Base
16
+ @database_name ||= base_class? ? name.underscore.pluralize : super
17
+ end
18
+ end
19
+
20
+ def self.base_class?
21
+ self == base_class
22
+ end
23
+
24
+ def self.base_class(klass = self)
25
+ if klass == ActiveDocument::Base or klass.superclass == ActiveDocument::Base
26
+ klass
27
+ else
28
+ base_class(klass.superclass)
29
+ end
30
+ end
31
+
32
+ @@environment = {}
33
+ def self.environment
34
+ @@environment[path] ||= ActiveDocument::Environment.new(path)
35
+ end
36
+
37
+ def self.transaction(&block)
38
+ environment.transaction(&block)
39
+ end
40
+
41
+ def self.create(*args)
42
+ model = new(*args)
43
+ model.save
44
+ model
45
+ end
46
+
47
+ def self.index_by(field, opts = {})
48
+ field = field.to_sym
49
+ raise "index on #{field} already exists" if databases[field]
50
+ databases[field] = ActiveDocument::Database.new(opts.merge(:field => field, :model_class => self))
51
+ end
52
+
53
+ def self.databases
54
+ @databases ||= { :id => ActiveDocument::Database.new(:model_class => self, :unique => true) }
55
+ end
56
+
57
+ def self.open_database
58
+ environment.open
59
+ databases[:id].open # Must be opened first for associate to work.
60
+ databases.values.each {|database| database.open}
61
+ end
62
+
63
+ def self.close_database
64
+ databases.values.each {|database| database.close}
65
+ environment.close
66
+ end
67
+
68
+ def self.database(field = :id)
69
+ field = field.to_sym
70
+ database = databases[field]
71
+ database ||= base_class.database(field) unless base_class?
72
+ database
73
+ end
74
+
75
+ def self.find_by(field, *keys)
76
+ opts = keys.last.kind_of?(Hash) ? keys.pop : {}
77
+ database(field).find(keys, opts)
78
+ end
79
+
80
+ def self.find(key, opts = {})
81
+ doc = database.find([key], opts).first
82
+ raise ActiveDocument::DocumentNotFound, "Couldn't find #{name} with key #{key}" unless doc
83
+ doc
84
+ end
85
+
86
+ def self.method_missing(method_name, *args)
87
+ method_name = method_name.to_s
88
+ if method_name =~ /^find_by_(\w+)$/
89
+ field = $1.to_sym
90
+ return find_by(field, *args) if databases[field]
91
+ end
92
+ raise NoMethodError, "undefined method `#{method_name}' for #{self}"
93
+ end
94
+
95
+ def self.timestamps
96
+ reader(:created_at, :updated_at)
97
+ end
98
+
99
+ def self.reader(*attrs)
100
+ attrs.each do |attr|
101
+ define_method(attr) do
102
+ attributes[attr]
103
+ end
104
+ end
105
+ end
106
+
107
+ def self.writer(*attrs)
108
+ attrs.each do |attr|
109
+ define_method("#{attr}=") do |value|
110
+ raise 'cannot modify readonly document' if readonly?
111
+ attributes[attr] = value
112
+ end
113
+ end
114
+ end
115
+
116
+ def self.accessor(*attrs)
117
+ reader(*attrs)
118
+ writer(*attrs)
119
+ end
120
+
121
+ def initialize(attributes = {})
122
+ if attributes.kind_of?(String)
123
+ @attributes, @saved_attributes = Marshal.load(attributes)
124
+ else
125
+ @attributes = attributes
126
+ end
127
+ end
128
+
129
+ attr_reader :saved_attributes
130
+
131
+ def attributes
132
+ @attributes ||= Marshal.load(Marshal.dump(saved_attributes))
133
+ end
134
+
135
+ def ==(other)
136
+ return false if other.nil?
137
+ attributes == other.attributes
138
+ end
139
+
140
+ def new_record?
141
+ @saved_attributes.nil?
142
+ end
143
+
144
+ def changed?(field = nil)
145
+ return false unless @attributes and @saved_attributes
146
+
147
+ if field
148
+ attributes[field] != saved_attributes[field]
149
+ else
150
+ attributes != saved_attributes
151
+ end
152
+ end
153
+
154
+ def save
155
+ attributes[:updated_at] = Time.now if respond_to?(:updated_at)
156
+ attributes[:created_at] = Time.now if respond_to?(:created_at) and new_record?
157
+ @saved_attributes = attributes
158
+ @attributes = nil
159
+ self.class.database.save(self)
160
+
161
+ true
162
+ end
163
+
164
+ def _dump(ignored)
165
+ Marshal.dump([@attributes, @saved_attributes])
166
+ end
167
+
168
+ def self._load(data)
169
+ new(data)
170
+ end
171
+ end
@@ -0,0 +1,124 @@
1
+ class ActiveDocument::Database
2
+ def initialize(opts)
3
+ @model_class = opts[:model_class]
4
+ @field = opts[:field]
5
+ @unique = opts[:unique]
6
+ @suffix = opts[:suffix] || (@field ? "by_#{@field}" : nil)
7
+ at_exit { close }
8
+ end
9
+
10
+ attr_accessor :model_class, :field, :db, :suffix
11
+
12
+ def unique?
13
+ @unique
14
+ end
15
+
16
+ def environment
17
+ model_class.environment
18
+ end
19
+
20
+ def primary_db
21
+ model_class.database.db if field
22
+ end
23
+
24
+ def name
25
+ @name ||= [model_class.database_name, suffix].compact.join('_')
26
+ end
27
+
28
+ def transaction
29
+ environment.transaction
30
+ end
31
+
32
+ def find(keys, opts = {}, &block)
33
+ models = block_given? ? BlockArray.new(block) : []
34
+
35
+ keys.uniq.each do |key|
36
+ if key.kind_of?(Range)
37
+ # Fetch a range of keys.
38
+ cursor = db.cursor(transaction, 0)
39
+ first = Tuple.dump(key.first)
40
+ last = Tuple.dump(key.last)
41
+ k,v = cursor.get(first, nil, Bdb::DB_SET_RANGE)
42
+ while key.exclude_end? ? k < last : k <= last
43
+ models << Marshal.load(v)
44
+ break if opts[:limit] and models.size == opts[:limit]
45
+ k, v = cursor.get(nil, nil, Bdb::DB_NEXT)
46
+ break unless k
47
+ end
48
+ cursor.close
49
+ else
50
+ if unique?
51
+ # There can only be one item for each key.
52
+ data = db.get(transaction, Tuple.dump(key), nil, 0)
53
+ models << Marshal.load(data) if data
54
+ else
55
+ # Have to use a cursor because there may be multiple items with each key.
56
+ cursor = db.cursor(transaction, 0)
57
+ k,v = cursor.get(Tuple.dump(key), nil, Bdb::DB_SET)
58
+ while k
59
+ models << Marshal.load(v)
60
+ break if opts[:limit] and models.size == opts[:limit]
61
+ k,v = cursor.get(nil, nil, Bdb::DB_NEXT_DUP)
62
+ end
63
+ cursor.close
64
+ end
65
+ end
66
+ break if opts[:limit] and models.size == opts[:limit]
67
+ end
68
+
69
+ block_given? ? nil : models
70
+ end
71
+
72
+ def save(model)
73
+ id = Tuple.dump(model.id)
74
+ data = Marshal.dump(model)
75
+ db.put(nil, id, data, 0)
76
+ end
77
+
78
+ def open
79
+ if @db.nil?
80
+ @db = environment.db
81
+ @db.flags = Bdb::DB_DUPSORT unless unique?
82
+ @db.open(nil, name, nil, Bdb::Db::BTREE, Bdb::DB_CREATE | Bdb::DB_AUTO_COMMIT, 0)
83
+
84
+ if primary_db
85
+ index_callback = lambda do |db, key, data|
86
+ model = Marshal.load(data)
87
+ return unless model.kind_of?(model_class)
88
+
89
+ index_key = model.send(field)
90
+ if index_key.kind_of?(Array)
91
+ # Index multiple keys. If the key is an array, you must wrap it with an outer array.
92
+ index_key.collect {|k| Tuple.dump(k)}
93
+ elsif index_key
94
+ # Index a single key.
95
+ Tuple.dump(index_key)
96
+ end
97
+ end
98
+
99
+ primary_db.associate(nil, @db, 0, index_callback)
100
+ end
101
+ end
102
+ end
103
+
104
+ def close
105
+ if @db
106
+ @db.close(0)
107
+ @db = nil
108
+ end
109
+ end
110
+ end
111
+
112
+ # This allows us to support a block in find without changing the syntax.
113
+ class BlockArray
114
+ def initialize(block)
115
+ @block = block
116
+ @size = 0
117
+ end
118
+ attr_reader :size
119
+
120
+ def <<(item)
121
+ @size += 1
122
+ @block.call(item)
123
+ end
124
+ end
@@ -0,0 +1,49 @@
1
+ class ActiveDocument::Environment
2
+ def initialize(path)
3
+ @path = path
4
+ at_exit { close }
5
+ end
6
+
7
+ attr_reader :path, :env
8
+
9
+ def db
10
+ env.db
11
+ end
12
+
13
+ def open
14
+ if @env.nil?
15
+ @env = Bdb::Env.new(0)
16
+ env_flags = Bdb::DB_CREATE | # Create the environment if it does not already exist.
17
+ Bdb::DB_INIT_TXN | # Initialize transactions
18
+ Bdb::DB_INIT_LOCK | # Initialize locking.
19
+ Bdb::DB_INIT_LOG | # Initialize logging
20
+ Bdb::DB_INIT_MPOOL # Initialize the in-memory cache.
21
+ @env.open(path, env_flags, 0);
22
+ end
23
+ end
24
+
25
+ def close
26
+ if @env
27
+ @env.close
28
+ @env = nil
29
+ end
30
+ end
31
+
32
+ def transaction
33
+ if block_given?
34
+ parent = @transaction
35
+ @transaction = env.txn_begin(nil, 0)
36
+ begin
37
+ yield
38
+ @transaction.commit(0)
39
+ rescue Exception => e
40
+ @transaction.abort
41
+ raise e
42
+ ensure
43
+ @transaction = parent
44
+ end
45
+ else
46
+ @transaction
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveDocument
2
+ class ActiveDocumentError < StandardError; end
3
+ class DocumentNotFound < ActiveDocumentError; end
4
+ end
5
+
6
+ require 'bdb'
7
+ require 'tuple'
8
+ require 'active_support/inflector'
9
+ require 'active_document/database'
10
+ require 'active_document/environment'
11
+ require 'active_document/base'
@@ -0,0 +1,69 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ BDB_PATH = File.dirname(__FILE__) + '/tmp'
4
+
5
+ class Foo < ActiveDocument::Base
6
+ path BDB_PATH
7
+ accessor :foo, :bar, :id
8
+
9
+ index_by :foo
10
+ index_by :bar, :unique => true
11
+ end
12
+
13
+ class ActiveDocumentTest < Test::Unit::TestCase
14
+ context 'with db open' do
15
+ setup do
16
+ FileUtils.mkdir BDB_PATH
17
+ Foo.open_database
18
+ end
19
+
20
+ teardown do
21
+ Foo.close_database
22
+ FileUtils.rmtree BDB_PATH
23
+ end
24
+
25
+ should 'find in database after save' do
26
+ f = Foo.new(:foo => 'BAR', :id => 1)
27
+ f.save
28
+
29
+ assert_equal f, Foo.find(1)
30
+ end
31
+
32
+ should 'raise exception if not found' do
33
+ assert_raises(ActiveDocument::DocumentNotFound) do
34
+ Foo.find(7)
35
+ end
36
+ end
37
+
38
+ should 'find_by_id' do
39
+ f = Foo.new(:foo => 'BAR', :id => 1)
40
+ f.save
41
+
42
+ assert_equal f, Foo.find_by_id(1).first
43
+ end
44
+
45
+ should 'find by secondary indexes' do
46
+ f1 = Foo.new(:foo => 'BAR', :bar => 'FOO', :id => 1)
47
+ f1.save
48
+
49
+ f2 = Foo.new(:foo => 'BAR', :bar => 'FU', :id => 2)
50
+ f2.save
51
+
52
+ assert_equal f1, Foo.find_by_bar('FOO').first
53
+ assert_equal f2, Foo.find_by_bar('FU').first
54
+ assert_equal [f1,f2], Foo.find_by_foo('BAR')
55
+ end
56
+
57
+ should 'find by range' do
58
+ (1..20).each do |i|
59
+ Foo.new(:id => i, :foo => "foo-#{i}").save
60
+ end
61
+
62
+ assert_equal (5..17).to_a, Foo.find_by_id(5..17).collect {|f| f.id}
63
+ assert_equal (5..14).to_a, Foo.find_by_id(5..17, :limit => 10).collect {|f| f.id}
64
+
65
+ # Mixed keys and ranges.
66
+ assert_equal (1..4).to_a + (16..20).to_a, Foo.find_by_id(1..3, 4, 16..20).collect {|f| f.id}
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require 'pp'
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
8
+ require 'active_document'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ninjudd-active_document
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Justin Balthrop
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-19 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: TODO
17
+ email: justin@geni.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - VERSION.yml
26
+ - lib/active_document
27
+ - lib/active_document/base.rb
28
+ - lib/active_document/database.rb
29
+ - lib/active_document/environment.rb
30
+ - lib/active_document.rb
31
+ - test/active_document_test.rb
32
+ - test/test_helper.rb
33
+ has_rdoc: true
34
+ homepage: http://github.com/ninjudd/active_document
35
+ licenses:
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --inline-source
39
+ - --charset=UTF-8
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.3.5
58
+ signing_key:
59
+ specification_version: 2
60
+ summary: TODO
61
+ test_files: []
62
+