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 +5 -0
- data/kyoto_record.gemspec +12 -0
- data/kyoto_record.rb +218 -0
- data/kyoto_record_spec.rb +179 -0
- data/watch.rb +20 -0
- metadata +60 -0
data/Gemfile
ADDED
@@ -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
|
+
|