kyoto_record 0.0.1

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