state 0.4.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/README +160 -0
- data/db +0 -0
- data/gemspec.rb +39 -0
- data/gen_readme.rb +35 -0
- data/install.rb +214 -0
- data/lib/state.rb +213 -0
- data/lib/state.rb.bak +85 -0
- data/sample/a.rb +31 -0
- data/sample/b.rb +16 -0
- data/sample/c.rb +10 -0
- data/sample/d.rb +29 -0
- data/spec/helper.rb +51 -0
- data/spec/state.rb +274 -0
- metadata +89 -0
data/README
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
NAME
|
2
|
+
state
|
3
|
+
|
4
|
+
SYNOPSIS
|
5
|
+
platform agnostic persistent state for ruby processes based on sqlite
|
6
|
+
|
7
|
+
DESCRIPTION
|
8
|
+
state provides an extremely simple to use state mechanism for ruby programs
|
9
|
+
which require state between invocations. the storage is based on sqlite and
|
10
|
+
is therefore platform agnostic. using state is no more difficult that using
|
11
|
+
a hash - only that hash will persist between invocations your ruby program.
|
12
|
+
see the samples and specs for details.
|
13
|
+
|
14
|
+
INSTALL
|
15
|
+
gem install state
|
16
|
+
|
17
|
+
URIS
|
18
|
+
http://codeforpeople.com/lib/ruby/
|
19
|
+
http://rubyforge.org/projects/codeforpeople/
|
20
|
+
|
21
|
+
SAMPLES
|
22
|
+
|
23
|
+
<========< sample/a.rb >========>
|
24
|
+
|
25
|
+
~ > cat sample/a.rb
|
26
|
+
|
27
|
+
require 'state'
|
28
|
+
|
29
|
+
# state provides persistent state for processes. usage requires simply
|
30
|
+
# accesses the state in a hash-like way.
|
31
|
+
#
|
32
|
+
|
33
|
+
# one process can create state
|
34
|
+
#
|
35
|
+
child do
|
36
|
+
State.clear
|
37
|
+
State['key'] = 'value'
|
38
|
+
end
|
39
|
+
|
40
|
+
# and later processes can have access to it
|
41
|
+
#
|
42
|
+
2.times do |i|
|
43
|
+
child do
|
44
|
+
value = State['key']
|
45
|
+
puts "child[#{ i }] => #{ value.inspect }"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
BEGIN {
|
51
|
+
# we use fork just for demonstation, but this works on windows too ;-)
|
52
|
+
#
|
53
|
+
def child &block
|
54
|
+
Process.waitpid fork(&block)
|
55
|
+
end
|
56
|
+
}
|
57
|
+
|
58
|
+
|
59
|
+
~ > ruby sample/a.rb
|
60
|
+
|
61
|
+
child[0] => "value"
|
62
|
+
child[1] => "value"
|
63
|
+
|
64
|
+
|
65
|
+
<========< sample/b.rb >========>
|
66
|
+
|
67
|
+
~ > cat sample/b.rb
|
68
|
+
|
69
|
+
require 'state'
|
70
|
+
|
71
|
+
# state will store it's db in a subdirectory (.state) of your home directory,
|
72
|
+
# the default database is ~/.state/global, but you may specify a name to
|
73
|
+
# create a new database
|
74
|
+
#
|
75
|
+
|
76
|
+
db = State.for 'foobar'
|
77
|
+
|
78
|
+
db.clear
|
79
|
+
|
80
|
+
puts db.path
|
81
|
+
|
82
|
+
10.times{|i| db[i] = i}
|
83
|
+
|
84
|
+
puts db.keys.inspect
|
85
|
+
|
86
|
+
~ > ruby sample/b.rb
|
87
|
+
|
88
|
+
/Users/ahoward/.state/foobar
|
89
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
90
|
+
|
91
|
+
|
92
|
+
<========< sample/c.rb >========>
|
93
|
+
|
94
|
+
~ > cat sample/c.rb
|
95
|
+
|
96
|
+
require 'state'
|
97
|
+
|
98
|
+
# of course you can specify and absolute path
|
99
|
+
#
|
100
|
+
|
101
|
+
db = State.for :path => '/tmp/state'
|
102
|
+
|
103
|
+
db.clear
|
104
|
+
|
105
|
+
puts db.path
|
106
|
+
|
107
|
+
~ > ruby sample/c.rb
|
108
|
+
|
109
|
+
/tmp/state
|
110
|
+
|
111
|
+
|
112
|
+
<========< sample/d.rb >========>
|
113
|
+
|
114
|
+
~ > cat sample/d.rb
|
115
|
+
|
116
|
+
require 'state'
|
117
|
+
|
118
|
+
# in general the interface for state is like that of a hash, see the specs for
|
119
|
+
# more details
|
120
|
+
|
121
|
+
db = State.for 'foobar'
|
122
|
+
|
123
|
+
db.clear
|
124
|
+
|
125
|
+
10.times{|i| db[i] = i**2}
|
126
|
+
5.times{|i| db.delete i}
|
127
|
+
|
128
|
+
p db.keys
|
129
|
+
p db.values
|
130
|
+
|
131
|
+
# use the update method for atomic read-update of a key/val pair
|
132
|
+
|
133
|
+
db['key'] = 42
|
134
|
+
|
135
|
+
p :current => db['key']
|
136
|
+
|
137
|
+
db.update 'key' do |old|
|
138
|
+
p :old => old
|
139
|
+
new = 42.0
|
140
|
+
end
|
141
|
+
|
142
|
+
p :update => db['key']
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
~ > ruby sample/d.rb
|
147
|
+
|
148
|
+
[5, 6, 7, 8, 9]
|
149
|
+
[25, 36, 49, 64, 81]
|
150
|
+
{:current=>42}
|
151
|
+
{:old=>42}
|
152
|
+
{:update=>42.0}
|
153
|
+
|
154
|
+
|
155
|
+
HISTORY
|
156
|
+
0.4.2
|
157
|
+
initial version
|
158
|
+
|
159
|
+
AUTHORS
|
160
|
+
ara.t.howard
|
data/db
ADDED
File without changes
|
data/gemspec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#! /usr/bin/env gem build
|
2
|
+
|
3
|
+
lib, version = File::basename(File::dirname(File::expand_path(__FILE__))).split %r/-/, 2
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
Gem::Specification::new do |spec|
|
8
|
+
$VERBOSE = nil
|
9
|
+
|
10
|
+
shiteless = lambda do |list|
|
11
|
+
list.delete_if do |file|
|
12
|
+
file =~ %r/\.svn/ or
|
13
|
+
file =~ %r/\.tmp/
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
spec.name = lib
|
18
|
+
spec.version = version
|
19
|
+
spec.platform = Gem::Platform::RUBY
|
20
|
+
spec.summary = lib
|
21
|
+
|
22
|
+
spec.files = shiteless[Dir::glob("**/**")]
|
23
|
+
spec.executables = shiteless[Dir::glob("bin/*")].map{|exe| File::basename exe}
|
24
|
+
|
25
|
+
spec.require_path = "lib"
|
26
|
+
|
27
|
+
spec.has_rdoc = File::exist? "doc"
|
28
|
+
spec.test_suite_file = "test/#{ lib }.rb" if File::directory? "test"
|
29
|
+
#spec.add_dependency 'lib', '>= version'
|
30
|
+
spec.add_dependency 'sqlite3'
|
31
|
+
spec.add_dependency 'fattr'
|
32
|
+
|
33
|
+
spec.extensions << "extconf.rb" if File::exists? "extconf.rb"
|
34
|
+
|
35
|
+
spec.rubyforge_project = 'codeforpeople'
|
36
|
+
spec.author = "Ara T. Howard"
|
37
|
+
spec.email = "ara.t.howard@gmail.com"
|
38
|
+
spec.homepage = "http://codeforpeople.com/lib/ruby/#{ lib }/"
|
39
|
+
end
|
data/gen_readme.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
$VERBOSE=nil
|
6
|
+
|
7
|
+
def indent s, n = 2
|
8
|
+
ws = ' ' * n
|
9
|
+
s.gsub %r/^/, ws
|
10
|
+
end
|
11
|
+
|
12
|
+
template = IO::read 'README.tmpl'
|
13
|
+
|
14
|
+
samples = ''
|
15
|
+
prompt = '~ > '
|
16
|
+
|
17
|
+
Dir['sample*/*'].sort.each do |sample|
|
18
|
+
samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
|
19
|
+
|
20
|
+
cmd = "cat #{ sample }"
|
21
|
+
samples << indent(prompt + cmd, 2) << "\n\n"
|
22
|
+
samples << indent(`#{ cmd }`, 4) << "\n"
|
23
|
+
|
24
|
+
cmd = "ruby #{ sample }"
|
25
|
+
samples << indent(prompt + cmd, 2) << "\n\n"
|
26
|
+
|
27
|
+
cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -Ilib #{ sample })'"
|
28
|
+
#cmd = "ruby -Ilib #{ sample }"
|
29
|
+
samples << indent(`#{ cmd } 2>&1`, 4) << "\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
#samples.gsub! %r/^/, ' '
|
33
|
+
|
34
|
+
readme = template.gsub %r/^\s*@samples\s*$/, samples
|
35
|
+
print readme
|
data/install.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rbconfig'
|
3
|
+
require 'find'
|
4
|
+
require 'ftools'
|
5
|
+
require 'tempfile'
|
6
|
+
include Config
|
7
|
+
|
8
|
+
LIBDIR = "lib"
|
9
|
+
LIBDIR_MODE = 0644
|
10
|
+
|
11
|
+
BINDIR = "bin"
|
12
|
+
BINDIR_MODE = 0755
|
13
|
+
|
14
|
+
|
15
|
+
$srcdir = CONFIG["srcdir"]
|
16
|
+
$version = CONFIG["MAJOR"]+"."+CONFIG["MINOR"]
|
17
|
+
$libdir = File.join(CONFIG["libdir"], "ruby", $version)
|
18
|
+
$archdir = File.join($libdir, CONFIG["arch"])
|
19
|
+
$site_libdir = $:.find {|x| x =~ /site_ruby$/}
|
20
|
+
$bindir = CONFIG["bindir"] || CONFIG['BINDIR']
|
21
|
+
$ruby_install_name = CONFIG['ruby_install_name'] || CONFIG['RUBY_INSTALL_NAME'] || 'ruby'
|
22
|
+
$ruby_ext = CONFIG['EXEEXT'] || ''
|
23
|
+
$ruby = File.join($bindir, ($ruby_install_name + $ruby_ext))
|
24
|
+
|
25
|
+
if !$site_libdir
|
26
|
+
$site_libdir = File.join($libdir, "site_ruby")
|
27
|
+
elsif $site_libdir !~ %r/#{Regexp.quote($version)}/
|
28
|
+
$site_libdir = File.join($site_libdir, $version)
|
29
|
+
end
|
30
|
+
|
31
|
+
def install_rb(srcdir=nil, destdir=nil, mode=nil, bin=nil)
|
32
|
+
#{{{
|
33
|
+
path = []
|
34
|
+
dir = []
|
35
|
+
Find.find(srcdir) do |f|
|
36
|
+
next unless FileTest.file?(f)
|
37
|
+
next if (f = f[srcdir.length+1..-1]) == nil
|
38
|
+
next if (/CVS$/ =~ File.dirname(f))
|
39
|
+
next if (/\.svn/ =~ File.dirname(f))
|
40
|
+
next if f =~ %r/\.lnk/
|
41
|
+
next if f =~ %r/\.svn/
|
42
|
+
next if f =~ %r/\.swp/
|
43
|
+
next if f =~ %r/\.svn/
|
44
|
+
path.push f
|
45
|
+
dir |= [File.dirname(f)]
|
46
|
+
end
|
47
|
+
for f in dir
|
48
|
+
next if f == "."
|
49
|
+
next if f == "CVS"
|
50
|
+
File::makedirs(File.join(destdir, f))
|
51
|
+
end
|
52
|
+
for f in path
|
53
|
+
next if (/\~$/ =~ f)
|
54
|
+
next if (/^\./ =~ File.basename(f))
|
55
|
+
unless bin
|
56
|
+
File::install(File.join(srcdir, f), File.join(destdir, f), mode, true)
|
57
|
+
else
|
58
|
+
from = File.join(srcdir, f)
|
59
|
+
to = File.join(destdir, f)
|
60
|
+
shebangify(from) do |sf|
|
61
|
+
$deferr.print from, " -> ", File::catname(from, to), "\n"
|
62
|
+
$deferr.printf "chmod %04o %s\n", mode, to
|
63
|
+
File::install(sf, to, mode, false)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
#}}}
|
68
|
+
end
|
69
|
+
def shebangify f
|
70
|
+
#{{{
|
71
|
+
open(f) do |fd|
|
72
|
+
buf = fd.read 42
|
73
|
+
if buf =~ %r/^\s*#\s*!.*ruby/o
|
74
|
+
ftmp = Tempfile::new("#{ $$ }_#{ File::basename(f) }")
|
75
|
+
begin
|
76
|
+
fd.rewind
|
77
|
+
ftmp.puts "#!#{ $ruby }"
|
78
|
+
while((buf = fd.read(8192)))
|
79
|
+
ftmp.write buf
|
80
|
+
end
|
81
|
+
ftmp.close
|
82
|
+
yield ftmp.path
|
83
|
+
ensure
|
84
|
+
ftmp.close!
|
85
|
+
end
|
86
|
+
else
|
87
|
+
yield f
|
88
|
+
end
|
89
|
+
end
|
90
|
+
#}}}
|
91
|
+
end
|
92
|
+
def ARGV.switch
|
93
|
+
#{{{
|
94
|
+
return nil if self.empty?
|
95
|
+
arg = self.shift
|
96
|
+
return nil if arg == '--'
|
97
|
+
if arg =~ /^-(.)(.*)/
|
98
|
+
return arg if $1 == '-'
|
99
|
+
raise 'unknown switch "-"' if $2.index('-')
|
100
|
+
self.unshift "-#{$2}" if $2.size > 0
|
101
|
+
"-#{$1}"
|
102
|
+
else
|
103
|
+
self.unshift arg
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
#}}}
|
107
|
+
end
|
108
|
+
def ARGV.req_arg
|
109
|
+
#{{{
|
110
|
+
self.shift || raise('missing argument')
|
111
|
+
#}}}
|
112
|
+
end
|
113
|
+
def linkify d, linked = []
|
114
|
+
#--{{{
|
115
|
+
if test ?d, d
|
116
|
+
versioned = Dir[ File::join(d, "*-[0-9].[0-9].[0-9].rb") ]
|
117
|
+
versioned.each do |v|
|
118
|
+
src, dst = v, v.gsub(%r/\-[\d\.]+\.rb$/, '.rb')
|
119
|
+
lnk = nil
|
120
|
+
begin
|
121
|
+
if test ?l, dst
|
122
|
+
lnk = "#{ dst }.lnk"
|
123
|
+
puts "#{ dst } -> #{ lnk }"
|
124
|
+
File::rename dst, lnk
|
125
|
+
end
|
126
|
+
unless test ?e, dst
|
127
|
+
puts "#{ src } -> #{ dst }"
|
128
|
+
File::copy src, dst
|
129
|
+
linked << dst
|
130
|
+
end
|
131
|
+
ensure
|
132
|
+
if lnk
|
133
|
+
at_exit do
|
134
|
+
puts "#{ lnk } -> #{ dst }"
|
135
|
+
File::rename lnk, dst
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
linked
|
142
|
+
#--}}}
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
#
|
147
|
+
# main program
|
148
|
+
#
|
149
|
+
|
150
|
+
libdir = $site_libdir
|
151
|
+
bindir = $bindir
|
152
|
+
no_linkify = false
|
153
|
+
linked = nil
|
154
|
+
help = false
|
155
|
+
|
156
|
+
usage = <<-usage
|
157
|
+
#{ File::basename $0 }
|
158
|
+
-d, --destdir <destdir>
|
159
|
+
-l, --libdir <libdir>
|
160
|
+
-b, --bindir <bindir>
|
161
|
+
-r, --ruby <ruby>
|
162
|
+
-n, --no_linkify
|
163
|
+
-s, --sudo
|
164
|
+
-h, --help
|
165
|
+
usage
|
166
|
+
|
167
|
+
begin
|
168
|
+
while switch = ARGV.switch
|
169
|
+
case switch
|
170
|
+
when '-d', '--destdir'
|
171
|
+
libdir = ARGV.req_arg
|
172
|
+
when '-l', '--libdir'
|
173
|
+
libdir = ARGV.req_arg
|
174
|
+
when '-b', '--bindir'
|
175
|
+
bindir = ARGV.req_arg
|
176
|
+
when '-r', '--ruby'
|
177
|
+
$ruby = ARGV.req_arg
|
178
|
+
when '-n', '--no_linkify'
|
179
|
+
no_linkify = true
|
180
|
+
when '-s', '--sudo'
|
181
|
+
sudo = 'sudo'
|
182
|
+
when '-h', '--help'
|
183
|
+
help = true
|
184
|
+
else
|
185
|
+
raise "unknown switch #{switch.dump}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
rescue
|
189
|
+
STDERR.puts $!.to_s
|
190
|
+
STDERR.puts usage
|
191
|
+
exit 1
|
192
|
+
end
|
193
|
+
|
194
|
+
if help
|
195
|
+
STDOUT.puts usage
|
196
|
+
exit
|
197
|
+
end
|
198
|
+
|
199
|
+
system "#{ sudo } #{ $ruby } pre-install.rb" if test(?s, 'pre-install.rb')
|
200
|
+
|
201
|
+
unless no_linkify
|
202
|
+
linked = linkify('lib') + linkify('bin')
|
203
|
+
end
|
204
|
+
|
205
|
+
system "#{ $ruby } extconf.rb && make && #{ sudo } make install" if test(?s, 'extconf.rb')
|
206
|
+
|
207
|
+
install_rb(LIBDIR, libdir, LIBDIR_MODE)
|
208
|
+
install_rb(BINDIR, bindir, BINDIR_MODE, bin=true)
|
209
|
+
|
210
|
+
if linked
|
211
|
+
linked.each{|path| File::rm_f path}
|
212
|
+
end
|
213
|
+
|
214
|
+
system "#{ sudo } #{ $ruby } post-install.rb" if test(?s, 'post-install.rb')
|
data/lib/state.rb
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'sqlite3'
|
5
|
+
require 'fattr'
|
6
|
+
|
7
|
+
module State
|
8
|
+
def for *argv, &block
|
9
|
+
options = (Hash === argv.last ? argv.pop : {})
|
10
|
+
name = argv.first
|
11
|
+
name ||= 'global' if(options.empty? and name.nil?)
|
12
|
+
|
13
|
+
default_path = File.join(rootdir, clean(name)) if name
|
14
|
+
path = getopt(:path, options, default_path)
|
15
|
+
|
16
|
+
db.new path, options
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method 'new', 'for'
|
20
|
+
|
21
|
+
fattr :home do
|
22
|
+
home =
|
23
|
+
catch :home do
|
24
|
+
["HOME", "USERPROFILE"].each do |key|
|
25
|
+
throw(:home, ENV[key]) if ENV[key]
|
26
|
+
end
|
27
|
+
if ENV["HOMEDRIVE"] and ENV["HOMEPATH"]
|
28
|
+
throw(:home, "#{ ENV['HOMEDRIVE'] }:#{ ENV['HOMEPATH'] }")
|
29
|
+
end
|
30
|
+
File.expand_path("~") rescue(File::ALT_SEPARATOR ? "C:/" : "/")
|
31
|
+
end
|
32
|
+
File.expand_path home
|
33
|
+
end
|
34
|
+
|
35
|
+
fattr :dotdir do
|
36
|
+
'.state'
|
37
|
+
end
|
38
|
+
|
39
|
+
fattr :rootdir do
|
40
|
+
root = File.join home, dotdir
|
41
|
+
FileUtils.mkdir_p root
|
42
|
+
root
|
43
|
+
end
|
44
|
+
|
45
|
+
def clean name
|
46
|
+
fugly = %r/[^a-zA-Z0-9_-]/
|
47
|
+
name.gsub fugly, '_'
|
48
|
+
end
|
49
|
+
|
50
|
+
def getopt key, hash, default = nil
|
51
|
+
return hash.fetch(key) if hash.has_key?(key)
|
52
|
+
key = key.to_s
|
53
|
+
return hash.fetch(key) if hash.has_key?(key)
|
54
|
+
key = key.to_sym
|
55
|
+
return hash.fetch(key) if hash.has_key?(key)
|
56
|
+
default
|
57
|
+
end
|
58
|
+
|
59
|
+
class Db
|
60
|
+
fattr('path'){ }
|
61
|
+
fattr('db'){ SQLite3::Database.new(path) }
|
62
|
+
fattr('table'){ 'state' }
|
63
|
+
fattr('schema'){
|
64
|
+
<<-sql
|
65
|
+
create table #{ table }(
|
66
|
+
key blob,
|
67
|
+
val blob,
|
68
|
+
primary key(key)
|
69
|
+
)
|
70
|
+
sql
|
71
|
+
}
|
72
|
+
|
73
|
+
def initialize path, options = {}
|
74
|
+
self.path = path
|
75
|
+
db!
|
76
|
+
migrate!
|
77
|
+
end
|
78
|
+
|
79
|
+
def clear
|
80
|
+
db.execute "delete from #{ table }"
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete key
|
84
|
+
db.execute "delete from #{ table } where key=?", blob(key)
|
85
|
+
end
|
86
|
+
|
87
|
+
def size
|
88
|
+
db.query("select count(*) from #{ table }") do |result|
|
89
|
+
return result.each{|row| break Integer(row.first)}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def each
|
94
|
+
db.query("select * from #{ table }") do |result|
|
95
|
+
result.each do |row|
|
96
|
+
key, val, *ignored = row
|
97
|
+
yield load(key), load(val)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
include ::Enumerable
|
102
|
+
|
103
|
+
def keys
|
104
|
+
map{|pair| pair.first}
|
105
|
+
end
|
106
|
+
|
107
|
+
def values
|
108
|
+
map{|pair| pair.last}
|
109
|
+
end
|
110
|
+
|
111
|
+
def [] key
|
112
|
+
db.query("select val from #{ table } where key=?", blob(key)) do |result|
|
113
|
+
val = result.each{|row| break row.first}
|
114
|
+
return( val ? load(val) : val )
|
115
|
+
end
|
116
|
+
end
|
117
|
+
alias_method 'get', '[]'
|
118
|
+
|
119
|
+
def []= key, val
|
120
|
+
key, val = blob(key), blob(val)
|
121
|
+
|
122
|
+
transaction do
|
123
|
+
begin
|
124
|
+
statement = "insert into #{ table }(key, val) values (?, ?)"
|
125
|
+
db.execute statement, key, val
|
126
|
+
return val
|
127
|
+
rescue Object
|
128
|
+
begin
|
129
|
+
statement = "update #{ table } set val=? where key=?"
|
130
|
+
db.execute statement, val, key
|
131
|
+
return val
|
132
|
+
rescue Object
|
133
|
+
begin
|
134
|
+
statement = "delete from #{ table } where key=?"
|
135
|
+
db.execute statement, key
|
136
|
+
statement = "insert into #{ table }(key, val) values (?, ?)"
|
137
|
+
db.execute statement, key, val
|
138
|
+
return val
|
139
|
+
rescue Object
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
alias_method 'set', '[]='
|
146
|
+
|
147
|
+
def update key, *val, &block
|
148
|
+
if block and not val.empty?
|
149
|
+
raise ArgumentError, 'both block and value'
|
150
|
+
end
|
151
|
+
|
152
|
+
if val.empty? and block.nil?
|
153
|
+
raise ArgumentError, 'no update value'
|
154
|
+
end
|
155
|
+
|
156
|
+
transaction do
|
157
|
+
old = get key
|
158
|
+
val = (block ? block.call(old) : val.first)
|
159
|
+
set key, val
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def transaction &block
|
164
|
+
if defined?(@transaction) and @transaction
|
165
|
+
block.call db
|
166
|
+
else
|
167
|
+
@transaction = true
|
168
|
+
begin
|
169
|
+
db.transaction{ block.call db }
|
170
|
+
ensure
|
171
|
+
@transaction = false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def blob val
|
177
|
+
SQLite3::Blob.new dump(val)
|
178
|
+
end
|
179
|
+
|
180
|
+
def dump value
|
181
|
+
Marshal.dump(value)
|
182
|
+
end
|
183
|
+
|
184
|
+
def load value
|
185
|
+
Marshal.load value
|
186
|
+
end
|
187
|
+
|
188
|
+
def migrate!
|
189
|
+
begin
|
190
|
+
db.execute "select count(*) from #{ table }"
|
191
|
+
rescue Object
|
192
|
+
db.execute schema
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def db
|
198
|
+
Db
|
199
|
+
end
|
200
|
+
|
201
|
+
fattr(:global){ State.for('global') }
|
202
|
+
|
203
|
+
def method_missing m, *a, &b
|
204
|
+
super unless global.respond_to?(m)
|
205
|
+
global.send(m, *a, &b)
|
206
|
+
end
|
207
|
+
|
208
|
+
extend self
|
209
|
+
end
|
210
|
+
|
211
|
+
def State *a, &b
|
212
|
+
State.for *a, &b
|
213
|
+
end
|
data/lib/state.rb.bak
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'amalgalite'
|
6
|
+
require 'sqlite'
|
7
|
+
require 'fattr'
|
8
|
+
|
9
|
+
module State
|
10
|
+
def for program = __FILE__
|
11
|
+
end
|
12
|
+
|
13
|
+
class Db
|
14
|
+
fattr('path'){ }
|
15
|
+
fattr('db'){ Amalgalite::Database.new(path) }
|
16
|
+
fattr('table'){ 'state' }
|
17
|
+
fattr('schema'){
|
18
|
+
<<-sql
|
19
|
+
create table #{ table }(
|
20
|
+
key blob,
|
21
|
+
value blob,
|
22
|
+
primary key(key)
|
23
|
+
)
|
24
|
+
sql
|
25
|
+
}
|
26
|
+
|
27
|
+
def initialize path
|
28
|
+
self.path = path
|
29
|
+
db!
|
30
|
+
migrate!
|
31
|
+
end
|
32
|
+
|
33
|
+
def []
|
34
|
+
end
|
35
|
+
|
36
|
+
def []= key, value
|
37
|
+
db.execute "insert into #{ table }(key, value) values ($key, $value)",
|
38
|
+
blob_pair(key, value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def blob_pair key, value
|
42
|
+
{ "$key" => blob_key(key), "$value" => blob_value(value) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def blob_key value
|
46
|
+
blob value, 'key'
|
47
|
+
end
|
48
|
+
|
49
|
+
def blob_value value
|
50
|
+
blob value, 'value'
|
51
|
+
end
|
52
|
+
|
53
|
+
def blob value, column
|
54
|
+
Amalgalite::Blob.new(
|
55
|
+
:io => io(dump(value)),
|
56
|
+
:column => column(column)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def io value
|
61
|
+
StringIO.new value
|
62
|
+
end
|
63
|
+
|
64
|
+
def dump value
|
65
|
+
Marshal.dump value
|
66
|
+
end
|
67
|
+
|
68
|
+
def column column
|
69
|
+
db.schema.tables[table].columns[column]
|
70
|
+
end
|
71
|
+
|
72
|
+
def migrate!
|
73
|
+
unless db.schema.tables[table]
|
74
|
+
db.execute schema
|
75
|
+
db.reload_schema!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
extend self
|
81
|
+
end
|
82
|
+
|
83
|
+
def State *a, &b
|
84
|
+
State.for *a, &b
|
85
|
+
end
|
data/sample/a.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'state'
|
2
|
+
|
3
|
+
# state provides persistent state for processes. usage requires simply
|
4
|
+
# accesses the state in a hash-like way.
|
5
|
+
#
|
6
|
+
|
7
|
+
# one process can create state
|
8
|
+
#
|
9
|
+
child do
|
10
|
+
State.clear
|
11
|
+
State['key'] = 'value'
|
12
|
+
end
|
13
|
+
|
14
|
+
# and later processes can have access to it
|
15
|
+
#
|
16
|
+
2.times do |i|
|
17
|
+
child do
|
18
|
+
value = State['key']
|
19
|
+
puts "child[#{ i }] => #{ value.inspect }"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
BEGIN {
|
25
|
+
# we use fork just for demonstation, but this works on windows too ;-)
|
26
|
+
#
|
27
|
+
def child &block
|
28
|
+
Process.waitpid fork(&block)
|
29
|
+
end
|
30
|
+
}
|
31
|
+
|
data/sample/b.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'state'
|
2
|
+
|
3
|
+
# state will store it's db in a subdirectory (.state) of your home directory,
|
4
|
+
# the default database is ~/.state/global, but you may specify a name to
|
5
|
+
# create a new database
|
6
|
+
#
|
7
|
+
|
8
|
+
db = State.for 'foobar'
|
9
|
+
|
10
|
+
db.clear
|
11
|
+
|
12
|
+
puts db.path
|
13
|
+
|
14
|
+
10.times{|i| db[i] = i}
|
15
|
+
|
16
|
+
puts db.keys.inspect
|
data/sample/c.rb
ADDED
data/sample/d.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'state'
|
2
|
+
|
3
|
+
# in general the interface for state is like that of a hash, see the specs for
|
4
|
+
# more details
|
5
|
+
|
6
|
+
db = State.for 'foobar'
|
7
|
+
|
8
|
+
db.clear
|
9
|
+
|
10
|
+
10.times{|i| db[i] = i**2}
|
11
|
+
5.times{|i| db.delete i}
|
12
|
+
|
13
|
+
p db.keys
|
14
|
+
p db.values
|
15
|
+
|
16
|
+
# use the update method for atomic read-update of a key/val pair
|
17
|
+
|
18
|
+
db['key'] = 42
|
19
|
+
|
20
|
+
p :current => db['key']
|
21
|
+
|
22
|
+
db.update 'key' do |old|
|
23
|
+
p :old => old
|
24
|
+
new = 42.0
|
25
|
+
end
|
26
|
+
|
27
|
+
p :update => db['key']
|
28
|
+
|
29
|
+
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Spec
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
@rootdir ||= File.expand_path(File.dirname(File.dirname(__FILE__)))
|
5
|
+
@libdir ||= File.join(@rootdir, 'lib')
|
6
|
+
@bindir ||= File.join(@rootdir, 'bin')
|
7
|
+
@specdir ||= File.join(@rootdir, 'spec')
|
8
|
+
|
9
|
+
def dir which, *parts
|
10
|
+
ivar = "@#{ which }dir"
|
11
|
+
dir = instance_variable_get(ivar)
|
12
|
+
if parts.empty?
|
13
|
+
dir
|
14
|
+
else
|
15
|
+
parts = parts.flatten.compacet.map{|part| part.to_s}
|
16
|
+
File.join(dir, *parts)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
%w( root lib bin spec ).each do |which|
|
21
|
+
define_method("#{ which }dir"){|*parts| dir which, *parts}
|
22
|
+
end
|
23
|
+
|
24
|
+
def pid
|
25
|
+
@pid ||= Process.pid
|
26
|
+
end
|
27
|
+
|
28
|
+
def fu
|
29
|
+
FileUtils
|
30
|
+
end
|
31
|
+
|
32
|
+
def tmpdir &block
|
33
|
+
path = File.join specdir, 'tmp', pid.to_s
|
34
|
+
fu.mkdir_p path
|
35
|
+
at_exit{ fu.rm_rf path}
|
36
|
+
if block
|
37
|
+
begin
|
38
|
+
Dir.chdir(path, &block)
|
39
|
+
ensure
|
40
|
+
fu.rm_rf path
|
41
|
+
end
|
42
|
+
else
|
43
|
+
path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
extend self
|
48
|
+
|
49
|
+
STDOUT.sync = true
|
50
|
+
STDERR.sync = true
|
51
|
+
end
|
data/spec/state.rb
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
#! /usr/bin/env bacon
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bacon'
|
5
|
+
|
6
|
+
describe 'state db' do
|
7
|
+
it 'should initialize a new db when none exists' do
|
8
|
+
Spec.tmpdir do
|
9
|
+
path = 'state.db'
|
10
|
+
db = State::Db.new path
|
11
|
+
db.path.should == path
|
12
|
+
exists = test(?s, db.path) ? true : false
|
13
|
+
exists.should == true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should re-use an existing db' do
|
18
|
+
path = 'state.db'
|
19
|
+
buf = nil
|
20
|
+
|
21
|
+
Spec.tmpdir do
|
22
|
+
db = State::Db.new path
|
23
|
+
buf = IO.read db.path
|
24
|
+
end
|
25
|
+
|
26
|
+
Spec.tmpdir do
|
27
|
+
open(path,'wb+'){|f| f.write buf}
|
28
|
+
|
29
|
+
before = File.stat path
|
30
|
+
|
31
|
+
db = State::Db.new path
|
32
|
+
db.path.should == path
|
33
|
+
exists = test(?s, db.path) ? true : false
|
34
|
+
exists.should == true
|
35
|
+
|
36
|
+
after = File.stat path
|
37
|
+
|
38
|
+
before.should == after
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should store key/value pairs' do
|
43
|
+
Spec.tmpdir do
|
44
|
+
db = State::Db.new 'state.db'
|
45
|
+
|
46
|
+
hash = {
|
47
|
+
'a' => 'a',
|
48
|
+
'b' => 0,
|
49
|
+
'c' => [],
|
50
|
+
'd' => hash
|
51
|
+
}
|
52
|
+
|
53
|
+
hash.keys.sort.each do |k|
|
54
|
+
v = hash[k]
|
55
|
+
raised =
|
56
|
+
begin
|
57
|
+
db[k] = v
|
58
|
+
false
|
59
|
+
rescue Object
|
60
|
+
true
|
61
|
+
end
|
62
|
+
raised.should == false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should read key/value pairs' do
|
68
|
+
Spec.tmpdir do
|
69
|
+
db = State::Db.new 'state.db'
|
70
|
+
|
71
|
+
hash = {
|
72
|
+
'a' => 'a',
|
73
|
+
'b' => 0,
|
74
|
+
'c' => [],
|
75
|
+
'd' => hash
|
76
|
+
}
|
77
|
+
|
78
|
+
hash.keys.sort.each do |k|
|
79
|
+
v = hash[k]
|
80
|
+
db[k] = v
|
81
|
+
v.should == db[k]
|
82
|
+
end
|
83
|
+
|
84
|
+
hash.keys.sort.each do |k|
|
85
|
+
v = hash[k]
|
86
|
+
v.should == db[k]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should clobber key/value pairs' do
|
92
|
+
Spec.tmpdir do
|
93
|
+
db = State::Db.new 'state.db'
|
94
|
+
|
95
|
+
hash = {
|
96
|
+
'a' => 'a',
|
97
|
+
'b' => 0,
|
98
|
+
'c' => [],
|
99
|
+
'd' => hash
|
100
|
+
}
|
101
|
+
|
102
|
+
hash.keys.sort.each do |k|
|
103
|
+
2.times do |i|
|
104
|
+
v = "#{ hash[k] }_#{ i }"
|
105
|
+
db[k] = v
|
106
|
+
db[k].should == v
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'should know how many key/value pairs it has' do
|
113
|
+
Spec.tmpdir do
|
114
|
+
db = State::Db.new 'state.db'
|
115
|
+
|
116
|
+
hash = {
|
117
|
+
'a' => 'a',
|
118
|
+
'b' => 0,
|
119
|
+
'c' => [],
|
120
|
+
'd' => hash
|
121
|
+
}
|
122
|
+
|
123
|
+
hash.keys.sort.each do |k|
|
124
|
+
db[k] = hash[k]
|
125
|
+
end
|
126
|
+
|
127
|
+
db.size.should == hash.size
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should be able to clear all key/value pairs' do
|
132
|
+
Spec.tmpdir do
|
133
|
+
db = State::Db.new 'state.db'
|
134
|
+
|
135
|
+
hash = {
|
136
|
+
'a' => 'a',
|
137
|
+
'b' => 0,
|
138
|
+
'c' => [],
|
139
|
+
'd' => hash
|
140
|
+
}
|
141
|
+
|
142
|
+
hash.keys.sort.each do |k|
|
143
|
+
db[k] = hash[k]
|
144
|
+
end
|
145
|
+
|
146
|
+
db.size.should == hash.size
|
147
|
+
|
148
|
+
db.clear
|
149
|
+
|
150
|
+
db.size.should == 0
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'should be able to delete specific key/value pairs' do
|
155
|
+
Spec.tmpdir do
|
156
|
+
db = State::Db.new 'state.db'
|
157
|
+
|
158
|
+
hash = {
|
159
|
+
'a' => 'a',
|
160
|
+
'b' => 0,
|
161
|
+
'c' => [],
|
162
|
+
'd' => hash
|
163
|
+
}
|
164
|
+
|
165
|
+
hash.keys.sort.each do |k|
|
166
|
+
db[k] = hash[k]
|
167
|
+
end
|
168
|
+
|
169
|
+
db.size.should == hash.size
|
170
|
+
|
171
|
+
hash.keys.sort.each do |k|
|
172
|
+
hash.delete k
|
173
|
+
db.delete k
|
174
|
+
db.size.should == hash.size
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should iterate with an each method, map, and be enumerable' do
|
180
|
+
Spec.tmpdir do
|
181
|
+
db = State::Db.new 'state.db'
|
182
|
+
|
183
|
+
(Enumerable === db).should == true
|
184
|
+
|
185
|
+
hash = {
|
186
|
+
'a' => 'a',
|
187
|
+
'b' => 0,
|
188
|
+
'c' => [],
|
189
|
+
'd' => hash
|
190
|
+
}
|
191
|
+
|
192
|
+
hash.keys.sort.each do |k|
|
193
|
+
db[k] = hash[k]
|
194
|
+
end
|
195
|
+
|
196
|
+
result = {}
|
197
|
+
db.each do |k,v|
|
198
|
+
result[k] = v
|
199
|
+
end
|
200
|
+
result.should == hash
|
201
|
+
|
202
|
+
db.map.should == hash.map
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should support atomic update of a key/value pair' do
|
207
|
+
Spec.tmpdir do
|
208
|
+
db = State::Db.new 'state.db'
|
209
|
+
|
210
|
+
hash = {
|
211
|
+
'a' => 'a',
|
212
|
+
'b' => 0,
|
213
|
+
'c' => [],
|
214
|
+
'd' => hash
|
215
|
+
}
|
216
|
+
|
217
|
+
hash.keys.sort.each do |k|
|
218
|
+
db.update k, hash[k]
|
219
|
+
db[k].should == hash[k]
|
220
|
+
end
|
221
|
+
|
222
|
+
hash.keys.sort.each do |k|
|
223
|
+
db.update k, hash[k]
|
224
|
+
db[k].should == hash[k]
|
225
|
+
end
|
226
|
+
|
227
|
+
hash.keys.sort.each do |k|
|
228
|
+
db.update(k){ hash[k] }
|
229
|
+
db[k].should == hash[k]
|
230
|
+
end
|
231
|
+
|
232
|
+
hash.keys.sort.each do |k|
|
233
|
+
v = hash[k].to_s + '!'
|
234
|
+
db.update(k){ v }
|
235
|
+
db[k].should == v
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe 'state' do
|
242
|
+
it 'can find a writable home directory' do
|
243
|
+
found = test(?w, State.home) ? true : false
|
244
|
+
found.should == true
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'can bootstrap a db into a subdir of the home directory based on a name' do
|
248
|
+
db = State 'foobar'
|
249
|
+
exists = test(?s, db.path) ? true : false
|
250
|
+
exists.should == true
|
251
|
+
File.dirname(db.path).should == State.rootdir
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'can bootstrap a db into a named path' do
|
255
|
+
Spec.tmpdir do
|
256
|
+
db = State :path => 'foobar'
|
257
|
+
exists = test(?s, db.path) ? true : false
|
258
|
+
exists.should == true
|
259
|
+
File.expand_path(db.path).should == File.expand_path('./foobar')
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
BEGIN {
|
266
|
+
rootdir = File.expand_path(File.dirname(File.dirname(__FILE__)))
|
267
|
+
|
268
|
+
libdir = File.join(rootdir, 'lib')
|
269
|
+
glob = File.join(libdir, '*.rb')
|
270
|
+
Dir[glob].each{|lib| require lib}
|
271
|
+
|
272
|
+
specdir = File.join(rootdir, 'spec')
|
273
|
+
require(File.join(specdir, 'helper'))
|
274
|
+
}
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ara T. Howard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-09-10 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: sqlite3
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: fattr
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: ara.t.howard@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- bin
|
45
|
+
- db
|
46
|
+
- gemspec.rb
|
47
|
+
- gen_readme.rb
|
48
|
+
- install.rb
|
49
|
+
- lib
|
50
|
+
- lib/state.rb
|
51
|
+
- lib/state.rb.bak
|
52
|
+
- README
|
53
|
+
- sample
|
54
|
+
- sample/a.rb
|
55
|
+
- sample/b.rb
|
56
|
+
- sample/c.rb
|
57
|
+
- sample/d.rb
|
58
|
+
- spec
|
59
|
+
- spec/helper.rb
|
60
|
+
- spec/state.rb
|
61
|
+
- spec/tmp
|
62
|
+
has_rdoc: false
|
63
|
+
homepage: http://codeforpeople.com/lib/ruby/state/
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
version:
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
version:
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project: codeforpeople
|
84
|
+
rubygems_version: 1.2.0
|
85
|
+
signing_key:
|
86
|
+
specification_version: 2
|
87
|
+
summary: state
|
88
|
+
test_files: []
|
89
|
+
|