ninjudd-active_document 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION.yml +4 -0
- data/lib/active_document/base.rb +171 -0
- data/lib/active_document/database.rb +124 -0
- data/lib/active_document/environment.rb +49 -0
- data/lib/active_document.rb +11 -0
- data/test/active_document_test.rb +69 -0
- data/test/test_helper.rb +8 -0
- metadata +62 -0
data/VERSION.yml
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
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
|
+
|