kyoto_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem 'directory_watcher'
4
+ gem 'rev'
5
+ gem 'rspec'
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "kyoto_record"
3
+ s.version = "0.0.1"
4
+ s.summary = "Persist your classes in Kyoto Cabinet."
5
+ s.description = "\"KyotoRecord\": Kyoto Cabinet wrapper for Ruby binding. Scannable. Indexable. A programming interface made for babies."
6
+ s.homepage = "http://github.com/thirdreplicator/kyoto_record"
7
+ s.authors = ["David Beckwith"]
8
+ s.email = "thirdreplicator@gmail.com"
9
+ s.require_paths = ["."]
10
+ s.files = ["kyoto_record.rb", "kyoto_record_spec.rb", "Gemfile", "watch.rb", "kyoto_record.gemspec"]
11
+ s.rubyforge_project = "nowarning"
12
+ end
data/kyoto_record.rb ADDED
@@ -0,0 +1,218 @@
1
+ require 'kyotocabinet'
2
+
3
+ module KyotoRecord
4
+
5
+ # Refactored out this module so that it can be reused in the class 'Index'
6
+ # as well as the module ClassMethds, which is directly enhancing the
7
+ # user-defined data model class.
8
+
9
+ module Cabinet
10
+ include KyotoCabinet
11
+
12
+ def find(id)
13
+ value = @db.get(id)
14
+ if value
15
+ obj = Marshal.load( @db.get(id) )
16
+ obj.id = id
17
+ obj
18
+ end
19
+ end
20
+
21
+ def scan(start_key=nil, limit=1/0.0, &block)
22
+ cur = @db.cursor
23
+ i = 0
24
+ cur.jump(start_key)
25
+
26
+ while (rec = cur.get(true)) && i < limit
27
+ key = rec[0]
28
+ if key != 'last_id'
29
+ i += 1
30
+ value = Marshal.load(rec[1])
31
+ block.call(value)
32
+ end
33
+ end
34
+ cur.disable
35
+ end
36
+
37
+ def scan_page(page, per_page=100, &block)
38
+ @recs_per_page ||= per_page
39
+ start_key = (page-1)*per_page + 1
40
+ limit = per_page
41
+ scan(start_key, limit, &block)
42
+ end
43
+
44
+ def get_attr(attr)
45
+ value = Marshal.load( @db.get( attr ) ) if kc.get(attr)
46
+ if value
47
+ return value
48
+ else
49
+ STDERR.printf("get error: %s\n", kc.error)
50
+ raise "Couldn't find value for attribute: #{attr}"
51
+ end
52
+ end
53
+
54
+ # Utilities
55
+ def open_db(base_name)
56
+ Dir.mkdir('data') if !Dir.exists?('data')
57
+ db = DB::new
58
+ unless db.open("./data/#{base_name}.kch", DB::OWRITER | DB::OCREATE)
59
+ STDERR.printf("open error: %s\n", db.error)
60
+ end
61
+ db
62
+ end
63
+
64
+ def close_db(base_name)
65
+ unless @db.close
66
+ STDERR.printf("close error: %s\n", db.error)
67
+ end
68
+ end
69
+
70
+ # Utilities
71
+
72
+ def set(k,v)
73
+ @db.set(k, Marshal.dump(v))
74
+ end
75
+
76
+ def get(k)
77
+ val = @db.get(k)
78
+ Marshal.load(val) if val
79
+ end
80
+
81
+ def set_raw(k,v)
82
+ @db.set(k,v)
83
+ end
84
+
85
+ def get_raw(k)
86
+ @db.get(k)
87
+ end
88
+
89
+ def last_id
90
+ get_raw(:last_id).to_i
91
+ end
92
+
93
+ def last_id=(i)
94
+ set_raw(:last_id, i)
95
+ end
96
+
97
+ def class_name
98
+ @class_name ||= self.to_s
99
+ end
100
+ end # Cabinet
101
+
102
+ # Reuse the Cabinet module to make indexes of attributes.
103
+ # Each index is a Kyoto Cabinet index. value -> id
104
+ # E.g. "David" -> 1
105
+ # For a "username" attribute on the class User, the index would be in
106
+ # ./data/User_username.kch
107
+ class Index
108
+ include Cabinet
109
+ attr_reader :klass, :attr, :base_name, :db
110
+
111
+ def initialize(klass, attribute)
112
+ @klass = klass
113
+ @attr = attribute
114
+ @base_name = klass.to_s + "_" + attribute.to_s
115
+ @db = open_db(@base_name)
116
+ end
117
+ end # class Index
118
+
119
+ module ClassMethods
120
+ # This is where attr_kyoto and index_kyoto go.
121
+ include Cabinet
122
+
123
+ def attr_kyoto( *attrs )
124
+ @db = open_db(class_name)
125
+
126
+ @attrs ||= {}
127
+ @indices ||= {}
128
+
129
+ # Don't redefine it if it was already defined.
130
+ if !self.respond_to?(:id)
131
+ # a general setter
132
+ define_method :set_attr do |k, v|
133
+ @values[k] = v
134
+ end
135
+
136
+ # a general getter
137
+ define_method :get_attr do |k|
138
+ @values[k]
139
+ end
140
+
141
+ def define_getter_and_setter(attr)
142
+ define_method :"#{attr}".to_s do
143
+ @values[attr]
144
+ end
145
+
146
+ define_method :"#{attr}=".to_s do |val|
147
+ set_attr(attr, val)
148
+ end
149
+ end
150
+
151
+ define_getter_and_setter(:id)
152
+ end # if
153
+
154
+ # In case multiple attributes were passed in, let's loop over each one.
155
+ attrs.each do |attr|
156
+ define_getter_and_setter(attr)
157
+ end
158
+ end # attr_kyoto
159
+
160
+ def index_kyoto( *attrs )
161
+ attrs.each do |attr|
162
+ @indices[attr] = Index.new(self, attr)
163
+ singleton = class << self; self; end
164
+ singleton.class_eval <<-EOM
165
+
166
+ def find_by_#{attr}(val)
167
+ self.find( @indices[\"#{attr}\".to_sym].get(val) )
168
+ end
169
+ EOM
170
+ end
171
+ end
172
+
173
+ def indices
174
+ @indices
175
+ end
176
+ end # ClassMethods
177
+
178
+ ### Instance Methods ###
179
+
180
+ def self.included(base)
181
+ base.extend(ClassMethods)
182
+ end
183
+
184
+ def initialize
185
+ @values ||= {}
186
+ super
187
+ end
188
+
189
+ def save
190
+ if !id
191
+ # TODO: use KyotoCabinet::DB#increment
192
+ id = self.class.last_id + 1
193
+ self.class.last_id = id
194
+ end
195
+
196
+ write_indices(id)
197
+ write_to_kyoto(id, self)
198
+ end
199
+
200
+ def write_indices(id)
201
+ self.class.indices.each do |attr, index|
202
+ index.set(@values[attr], id)
203
+ end
204
+ end
205
+
206
+ def write_to_kyoto(k, v)
207
+ set_error(k,v) unless self.class.set(k, v)
208
+ end
209
+
210
+ private
211
+
212
+ def set_error(k,v)
213
+ STDERR.printf("set error for (k,v)=(%s, %s): %s\n", k, v, kc.error)
214
+ end
215
+
216
+ end
217
+
218
+
@@ -0,0 +1,179 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require 'rspec'
4
+ require 'kyoto_record'
5
+
6
+ describe 'KyotoRecord module' do
7
+ before(:each) do
8
+ class A
9
+ include KyotoRecord
10
+ attr_kyoto :x
11
+ end
12
+ end
13
+
14
+ after(:each) do
15
+ `rm -rf ./data`
16
+ end
17
+
18
+ it "should have a database even if no instances were saved" do
19
+ File.exist?('./data/A.kch').should be_true
20
+ end
21
+
22
+ it "should retrieve 3 if 3 was saved into a kyoto attribute" do
23
+ @a = A.new
24
+ @a.x = 3
25
+ @a.save
26
+ A.find(1).should be_an_instance_of(A)
27
+ end
28
+
29
+ it "should retrieve 3 if 3 was saved into a kyoto attribute" do
30
+ @a = A.new
31
+ @a.x = 3
32
+ @a.save
33
+ A.find(1).x.should == 3
34
+ end
35
+
36
+ it "should retrieve a symbol :abc if the symbol :abc was saved" do
37
+ @a = A.new
38
+ @a.x = :abc
39
+ @a.save
40
+ A.find(1).x.should == :abc
41
+ end
42
+
43
+ it "should be able to save arbitrary objects" do
44
+ @a = A.new
45
+ @a.x = Time.now
46
+ @a.save
47
+ A.find(1).x.should be_an_instance_of( Time )
48
+ end
49
+
50
+ it "should have a last_id of nil if nothing has been saved yet." do
51
+ A.last_id.should == 0
52
+ end
53
+
54
+ it "should have a last_id of 1 if something has been saved." do
55
+ @a = A.new
56
+ @a.x = 1
57
+ @a.save
58
+ A.last_id.should == 1
59
+ end
60
+
61
+ it "should be able to set a particular value" do
62
+ A.set_raw(:xyz, 999)
63
+ A.get_raw(:xyz).should == "999" # Kyoto Cabinet only returns String or binary literals
64
+ end
65
+
66
+ it "should be able to find an instance by id" do
67
+ # Create two instances, set x, then save them.
68
+
69
+ @a = A.new
70
+ @a.x = 100
71
+ @a.save
72
+
73
+ @b = A.new
74
+ @b.x = 200
75
+ @b.save
76
+
77
+ A.find(1).x.should == 100
78
+ A.find(2).x.should == 200
79
+ end
80
+
81
+ it "should be able to set multiple KC attributes at once." do
82
+ class B
83
+ include KyotoRecord
84
+ attr_kyoto :x, :y, :z
85
+ end
86
+ b= B.new
87
+ b.should respond_to(:x)
88
+ b.should respond_to(:y)
89
+ b.should respond_to(:z)
90
+ end
91
+
92
+ it "should be able to set those variables just like normal" do
93
+ class B
94
+ include KyotoRecord
95
+ attr_kyoto :x, :y, :z
96
+ end
97
+ b= B.new
98
+ b = B.new
99
+ b.x = 5
100
+ b.y = :abc
101
+ b.z = "duck"
102
+ b.save
103
+
104
+ B.find(1).x.should == 5
105
+ B.find(1).y.should == :abc
106
+ B.find(1).z.should == "duck"
107
+ end
108
+
109
+ describe "Iterating over the records." do
110
+ before(:each) do
111
+ # Insert 100 records
112
+ 10.times do |i|
113
+ a = A.new
114
+ a.x = i+1
115
+ a.save
116
+ end
117
+ end
118
+
119
+ it "should be able to return a list of serialized objects" do
120
+ word = ""
121
+ A.scan do |z|
122
+ word += z.x.to_s
123
+ end
124
+ word.should == "12345678910"
125
+ end
126
+
127
+ it "should be able to start from the 2nd record" do
128
+ word = ""
129
+ A.scan(5) do |z|
130
+ word += z.x.to_s
131
+ end
132
+ word.should == "5678910"
133
+ end
134
+
135
+ it "should be able to stop after a given limit" do
136
+ word = ""
137
+ A.scan(2, 3) do |z|
138
+ word += z.x.to_s
139
+ end
140
+ word.should == "234"
141
+ end
142
+
143
+ it "should be able to scan by page" do
144
+ word = ""
145
+ A.scan_page(3, 2) do |z|
146
+ word += z.x.to_s
147
+ end
148
+ word.should == "56"
149
+ end
150
+ end
151
+ describe "Indexing of attributes." do
152
+ before(:each) do
153
+ class A
154
+ attr_kyoto :username
155
+ index_kyoto :username
156
+ end
157
+ end
158
+
159
+ it "should create a new database called './data/A_name.kch'" do
160
+ File.exist?('./data/A_username.kch').should be_true
161
+ end
162
+
163
+ it "should be able to look up a record by attribute value" do
164
+ a = A.new
165
+ a.username = "David"
166
+ a.save
167
+ A.indices[:username].should be_a_kind_of(::KyotoRecord::Index)
168
+ A.find_by_username("David").should be_a_kind_of( ::KyotoRecord )
169
+ A.find_by_username("David").id.should == 1
170
+ A.find_by_username("David").username.should == "David"
171
+ end
172
+ end
173
+
174
+ describe "Auxilliary functions" do
175
+ it "should be able to know it's own name as a string" do
176
+ A.class_name
177
+ end
178
+ end
179
+ end
data/watch.rb ADDED
@@ -0,0 +1,20 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require 'directory_watcher'
4
+
5
+ command = 'rspec -c *_spec.rb'
6
+
7
+ dw = DirectoryWatcher.new '.', :pre_load => true, :scanner => :rev
8
+ dw.glob = '**/*.rb'
9
+ dw.reset true
10
+ dw.interval = 1.0
11
+ dw.stable = 1.0
12
+ dw.add_observer do |*args|
13
+ args.each do |event|
14
+ system(command) if event.to_s =~ /stable/
15
+ end
16
+ end
17
+ dw.start
18
+ gets # when the user hits "enter" the script will terminate
19
+ dw.stop
20
+
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kyoto_record
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - David Beckwith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-03-02 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: "\"KyotoRecord\": Kyoto Cabinet wrapper for Ruby binding. Scannable. Indexable. A programming interface made for babies."
18
+ email: thirdreplicator@gmail.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - kyoto_record.rb
27
+ - kyoto_record_spec.rb
28
+ - Gemfile
29
+ - watch.rb
30
+ - kyoto_record.gemspec
31
+ has_rdoc: true
32
+ homepage: http://github.com/thirdreplicator/kyoto_record
33
+ licenses: []
34
+
35
+ post_install_message:
36
+ rdoc_options: []
37
+
38
+ require_paths:
39
+ - .
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ requirements: []
53
+
54
+ rubyforge_project: nowarning
55
+ rubygems_version: 1.5.3
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: Persist your classes in Kyoto Cabinet.
59
+ test_files: []
60
+