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 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
+