palsy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ html
19
+ coverage
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in palsy.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Erik Hollensbe
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Palsy - Set and forget it
2
+
3
+ Present ruby core data structures in a manner similar to perl's tie backed by a
4
+ SQLite database. Intended to be as simple as possible, sacrificing performance
5
+ and flexibility to do so.
6
+
7
+ It is not a 1:1 emulation of tie, as ruby cannot support this. What it does do
8
+ is provide a convincing enough facsimile by emulating the interface, and making
9
+ it easy to convert to native ruby types when that's not enough.
10
+
11
+ This library is completely unapologetic with regards to how little it does or
12
+ how slow it does things.
13
+
14
+ All writes are fully consistent, which is something SQLite gives us. Reads
15
+ always hit the database. This allows us to reason more clearly about how our
16
+ data persists, even in concurrent models where shared state can get very
17
+ complicated to use.
18
+
19
+ This was largely written to deal with problems I had to resolve in
20
+ [chef-workflow](https://github.com/chef-workflow). If you like the idea but
21
+ would like something more general or support for your favorite database, check
22
+ out [Moneta](https://github.com/minad/moneta) which is a little more geared
23
+ towards having a flexible backend.
24
+
25
+ ## Usage Example
26
+
27
+ Here's an example for dealing with several collections.
28
+
29
+ ```ruby
30
+ require 'palsy'
31
+
32
+ # only one palsy instance exists in a given ruby process
33
+ Palsy.database = 'test.db'
34
+ # first arg is table name, second is object name.
35
+ # standard datatypes are aggressive indexed and constrained. just because we
36
+ # don't care about performance doesn't mean we ignore easy wins.
37
+ set_a = Palsy::Set.new('some_sets', 'set_a')
38
+ set_b = Palsy::Set.new('some_sets', 'set_b')
39
+ set_c = Palsy::Set.new('some_sets', 'set_c')
40
+
41
+ set_a.add('something')
42
+ set_b.add('something_else')
43
+ # they are nestable like you'd expect.
44
+ set_c.add(set_a)
45
+
46
+ # enumerable works with collections like you'd expect. the results however are
47
+ # not palsy objects, so you cannot expect persistence by transformating them.
48
+ # consequently, most bang methods will not work as you expect either.
49
+ set_a.group_by(&:length)
50
+
51
+ # they are not ruby 'Set' objects, they just act like them.
52
+ # to get at more esoteric methods you'll need to convert them to the proper
53
+ # ruby type.
54
+ #
55
+ # For example, Set theory operators on Palsy::Set are not first class, you need
56
+ # to convert to a ruby Set first. Casting to array, hash and set is something
57
+ # we try to support wherever possible.
58
+ #
59
+ # Below we get the union of set_a and set_b
60
+
61
+ set_a.to_set | set_b.to_set
62
+ ```
63
+
64
+ ## Installation
65
+
66
+ Add this line to your application's Gemfile:
67
+
68
+ gem 'palsy'
69
+
70
+ And then execute:
71
+
72
+ $ bundle
73
+
74
+ Or install it yourself as:
75
+
76
+ $ gem install palsy
77
+
78
+ ## Contributing
79
+
80
+ 1. Fork it
81
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
82
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
83
+ 4. Push to the branch (`git push origin my-new-feature`)
84
+ 5. Create new Pull Request
85
+
86
+ Changes to author, license or version information without prior approval will
87
+ be rejected regardless of other changes.
88
+
89
+ ## Author
90
+
91
+ Erik Hollensbe <erik+github@hollensbe.org>
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.test_files = FileList['test/test_*.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ RDoc::Task.new do |rdoc|
12
+ rdoc.title = "Palsy: a simple sqlite layer for persisting data structures"
13
+ rdoc.rdoc_files.include("lib/**/*.rb")
14
+ rdoc.rdoc_files -= ["lib/palsy/version.rb"]
15
+ if ENV["RDOC_COVER"]
16
+ rdoc.options << "-C"
17
+ end
18
+ end
19
+
20
+ desc "run tests with coverage report"
21
+ task "test:coverage" do
22
+ ENV["COVERAGE"] = "1"
23
+ Rake::Task["test"].invoke
24
+ end
25
+
26
+ desc "run rdoc with coverage report"
27
+ task :rdoc_cov do
28
+ # ugh
29
+ ENV["RDOC_COVER"] = "1"
30
+ ruby "-S rake rerdoc"
31
+ end
32
+
33
+ desc "Clean up build products"
34
+ task :clean do
35
+ %w[html coverage].each do |dir|
36
+ FileUtils.rm_rf(dir)
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ require 'palsy/basic/generic'
2
+
3
+ class Palsy
4
+ #
5
+ # Base class for Collections. This includes Enumerable and makes the object
6
+ # name a required value. That's literally all it does.
7
+ #
8
+ # For more information on how to use this, see Palsy::Generic. If you want to
9
+ # exploit the Enumerable mixin, you need to define #each.
10
+ #
11
+ # Almost all Palsy::Collection objects are instantiated this way (using
12
+ # Palsy::Map as an example):
13
+ #
14
+ # obj = Palsy::Map.new("some_maps", "a_specific_map")
15
+ # obj[1] = 2
16
+ # obj[1] == 2 #=> true
17
+ #
18
+ # This will create a table called "some_maps" and all i/o will be directed to
19
+ # that table with an additional key of "a_specific_map". This allows you to
20
+ # coordinate multiple maps in a single table.
21
+ #
22
+ # Another example:
23
+ #
24
+ # obj1 = Palsy::Map.new("some_maps", "a_specific_map")
25
+ # obj2 = Palsy::Map.new("some_maps", "a_specific_map")
26
+ # obj3 = Palsy::Map.new("some_maps", "a_different_map")
27
+ #
28
+ # obj1 == obj2 #=> true
29
+ # obj1[1] = 2
30
+ # obj2[1] == 2 #=> also true
31
+ # obj3 == obj1 #=> false (different map keys)
32
+ # obj3[1] = 3
33
+ # obj2[1] == 3 #=> also false
34
+ #
35
+ class Collection < Generic
36
+ include Enumerable
37
+
38
+ #
39
+ # See the documentation for Palsy::Collection.
40
+ #
41
+ def initialize(table_name, object_name)
42
+ raise "an object_name must be provided!" unless object_name
43
+ super
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,89 @@
1
+ require 'palsy'
2
+
3
+ class Palsy
4
+ #
5
+ # Palsy::Generic is the base type for Palsy types. It implements the basic
6
+ # things needed for Palsy to work.
7
+ #
8
+ # Creating your own type is just a function of subclassing this and
9
+ # overriding the method Palsy::Generic#create_table, and adding methods you
10
+ # expect users to use to operate against the type. For example, this is what
11
+ # Palsy::Object#create_table looks like:
12
+ #
13
+ # class Palsy::Object < Palsy::Generic
14
+ # def create_table
15
+ # @db.execute %Q[
16
+ # create table if not exists #{@table_name} (
17
+ # id integer not null primary key autoincrement,
18
+ # key varchar(255) not null,
19
+ # value text not null,
20
+ # UNIQUE(key)
21
+ # )
22
+ # ]
23
+ # end
24
+ # end
25
+ #
26
+ class Generic
27
+ # The instance of Palsy. Overwriting this is probably not a good idea.
28
+ attr_accessor :db
29
+ # The name of the table this object is bound to.
30
+ attr_reader :table_name
31
+ # The name of the object this object is bound to inside the table. May be
32
+ # nil for certain types.
33
+ attr_reader :object_name
34
+
35
+ #
36
+ # This is the base constructor for all Palsy types and should always be run
37
+ # in subclasses before any action is taken by the class's initializer.
38
+ #
39
+ def initialize(table_name, object_name=nil)
40
+ raise "a table_name must be provided!" unless table_name
41
+
42
+ @table_name = table_name
43
+ @object_name = object_name
44
+ post_marshal_init
45
+ create_table
46
+ end
47
+
48
+ #
49
+ # Marshal helper to load objects.
50
+ #
51
+ def self._load(value)
52
+ obj = self.new(*Marshal.load(value))
53
+ return obj
54
+ end
55
+
56
+ #
57
+ # Marshal helper to dump objects. Only the table and object names are
58
+ # preserved.
59
+ #
60
+ def _dump(level)
61
+ self.db = nil
62
+ res = Marshal.dump([@table_name, @object_name])
63
+ post_marshal_init
64
+ return res
65
+ end
66
+
67
+ #
68
+ # Helper to manage the database instance for Marshal operations.
69
+ #
70
+ def post_marshal_init
71
+ @db = Palsy.instance
72
+ end
73
+
74
+ #
75
+ # Virtual method to define the create_table interface. Just raises,
76
+ # intended to be overridden.
77
+ #
78
+ def create_table
79
+ raise "Do not use the Generic type directly!"
80
+ end
81
+
82
+ #
83
+ # Equality method for comparing Palsy objects.
84
+ #
85
+ def ==(other)
86
+ [:db, :table_name, :object_name].all? { |x| self.send(x) == other.send(x) }
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,134 @@
1
+ require 'palsy/basic/collection'
2
+
3
+ class Palsy
4
+ #
5
+ # Palsy::List is a kind of Palsy::Collection and implements List semantics
6
+ # similar to Ruby's Array class. For more random-access behavior that Array
7
+ # provides, convert to a Ruby array with Palsy::List#to_a.
8
+ #
9
+ # All values of the list must be capable of being Marshalled.
10
+ #
11
+ # Here's an example:
12
+ #
13
+ # obj = Palsy::List.new("some_lists", "a_specific_list")
14
+ # obj.push 2
15
+ # obj.pop == 2 #=> true
16
+ #
17
+ # This will create a table called "some_lists" and all i/o will be directed to
18
+ # that table with an additional key of "a_specific_list". This allows you to
19
+ # coordinate multiple lists in a single table.
20
+ #
21
+ # Another example:
22
+ #
23
+ # obj1 = Palsy::List.new("some_lists", "a_specific_list")
24
+ # obj2 = Palsy::List.new("some_lists", "a_specific_list")
25
+ # obj3 = Palsy::List.new("some_lists", "a_different_list")
26
+ #
27
+ # obj1 == obj2 #=> true
28
+ # obj1.push 2
29
+ # obj2.pop == 2 #=> also true
30
+ # obj3 == obj1 #=> false (different list keys)
31
+ # obj.push 2
32
+ # obj3.push 3
33
+ # obj2.pop == 3 #=> also false
34
+ #
35
+ class List < Collection
36
+ #
37
+ # Add a value to the tail of the list.
38
+ #
39
+ def push(val)
40
+ @db.execute(
41
+ "insert into #{@table_name} (name, value) values (?, ?)",
42
+ [@object_name, Marshal.dump(val)]
43
+ )
44
+ end
45
+
46
+ alias << push
47
+
48
+ #
49
+ # Add a value to the head of the list.
50
+ #
51
+ def unshift(val)
52
+ replace([val] + to_a)
53
+ end
54
+
55
+ #
56
+ # Helper method for mutators.
57
+ #
58
+ def mutate(meth)
59
+ a = to_a
60
+ val = a.send(meth)
61
+ replace(a)
62
+ return val
63
+ end
64
+
65
+ #
66
+ # Returns the head of the list. Destructive.
67
+ #
68
+ def shift
69
+ mutate(:shift)
70
+ end
71
+
72
+ #
73
+ # Returns the tail of the list. Destructive.
74
+ #
75
+ def pop
76
+ mutate(:pop)
77
+ end
78
+
79
+ #
80
+ # Replace the list with the argument, which should be something that acts
81
+ # like an Enumerable.
82
+ #
83
+ def replace(ary)
84
+ clear
85
+
86
+ value_string = ("(?, ?)," * ary.count).chop
87
+
88
+ @db.execute(
89
+ "insert into #{@table_name} (name, value) values #{value_string}",
90
+ ary.map { |x| [@object_name, Marshal.dump(x)] }.flatten
91
+ )
92
+ end
93
+
94
+ #
95
+ # Empty the list.
96
+ #
97
+ def clear
98
+ @db.execute("delete from #{@table_name} where name=?", [@object_name])
99
+ end
100
+
101
+ #
102
+ # Yield each item in the list successively. Note that changes made to the
103
+ # items yielded will not persist, unless they are Palsy types themselves.
104
+ #
105
+ def each
106
+ to_a.each { |x| yield x }
107
+ end
108
+
109
+ #
110
+ # Convert the list to a Ruby Array.
111
+ #
112
+ def to_a
113
+ @db.execute(
114
+ "select value from #{@table_name} where name=? order by id",
115
+ [@object_name]
116
+ ).map { |x| Marshal.load(x.first) }
117
+ end
118
+
119
+ #
120
+ # Define the schema for this type.
121
+ #
122
+ def create_table
123
+ @db.execute <<-EOF
124
+ create table if not exists #{@table_name} (
125
+ id integer not null primary key autoincrement,
126
+ name varchar(255) not null,
127
+ value text not null
128
+ )
129
+ EOF
130
+
131
+ @db.execute "create index if not exists #{@table_name}_name_index on #{@table_name} (name)"
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,92 @@
1
+ require 'palsy/basic/set'
2
+
3
+ class Palsy
4
+ #
5
+ # Palsy::Map is an emulation of Ruby's Hash class, where objects are
6
+ # unordered and unique by the keyed value. They are a Palsy::Set and
7
+ # Palsy::Collection and have Enumerable support. Take a look at Palsy::Object
8
+ # which is similar but semantically different in a few ways.
9
+ #
10
+ # Palsy::Map keys and values must be marshallable, and their uniqueness for
11
+ # sanity's sake must also be enforced in their marshalled output.
12
+ #
13
+ # Here's an example:
14
+ #
15
+ # obj = Palsy::Map.new("some_maps", "a_specific_map")
16
+ # obj[1] = 2
17
+ # obj[1] == 2 #=> true
18
+ #
19
+ # This will create a table called "some_maps" and all i/o will be directed to
20
+ # that table with an additional key of "a_specific_map". This allows you to
21
+ # coordinate multiple maps in a single table.
22
+ #
23
+ # Another example:
24
+ #
25
+ # obj1 = Palsy::Map.new("some_maps", "a_specific_map")
26
+ # obj2 = Palsy::Map.new("some_maps", "a_specific_map")
27
+ # obj3 = Palsy::Map.new("some_maps", "a_different_map")
28
+ #
29
+ # obj1 == obj2 #=> true
30
+ # obj1[1] = 2
31
+ # obj2[1] == 2 #=> also true
32
+ # obj3 == obj1 #=> false (different map keys)
33
+ # obj3[1] = 3
34
+ # obj2[1] == 3 #=> also false
35
+ #
36
+ class Map < Set
37
+
38
+ #
39
+ # Obtain the value for a given key. Returns nil if the key does not have an
40
+ # entry.
41
+ #
42
+ def [](key)
43
+ value = @db.execute("select value from #{@table_name} where name=? and key=?", [@object_name, Marshal.dump(key)]).first.first rescue nil
44
+ return value && Marshal.load(value)
45
+ end
46
+
47
+ #
48
+ # Associate a value with a given key. Keys will be deleted from the
49
+ # database before attempting to write the new pair.
50
+ #
51
+ def []=(key, value)
52
+ delete(key)
53
+ @db.execute("insert into #{@table_name} (name, key, value) values (?, ?, ?)", [@object_name, Marshal.dump(key), Marshal.dump(value)])
54
+ value
55
+ end
56
+
57
+ #
58
+ # Successively yields key and value for each item in the map. Modifications
59
+ # to either the key or value yielded will not persist.
60
+ #
61
+ def each
62
+ keys.each do |key|
63
+ yield key, self[key]
64
+ end
65
+ end
66
+
67
+ #
68
+ # Convert this map to a Ruby Hash object.
69
+ #
70
+ def to_hash
71
+ rows = @db.execute("select key, value from #{@table_name} where name=?", [@object_name])
72
+ Hash[rows.map { |x| x.map { |y| Marshal.load(y) } }]
73
+ end
74
+
75
+ #
76
+ # Defines the schema for Maps.
77
+ #
78
+ def create_table
79
+ @db.execute <<-EOF
80
+ create table if not exists #{@table_name} (
81
+ id integer not null primary key autoincrement,
82
+ name varchar(255) not null,
83
+ key varchar(255) not null,
84
+ value text not null,
85
+ UNIQUE(name, key)
86
+ )
87
+ EOF
88
+
89
+ @db.execute "create index if not exists #{@table_name}_name_idx on #{@table_name} (name)"
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,64 @@
1
+ require 'palsy/basic/generic'
2
+
3
+ class Palsy
4
+ #
5
+ # Basic "symbol table"-ish object.
6
+ #
7
+ # The difference between using this over Palsy::Map is that objects take a
8
+ # full database table, treat all indexes as strings, and do not act like
9
+ # Palsy::Collection subclasses.
10
+ #
11
+ # Example:
12
+ #
13
+ # obj = Palsy::Object.new("object_table")
14
+ # obj["var1"] = 1
15
+ # obj["var2"] = 2
16
+ # obj["var1"] == 1 #=> true
17
+ #
18
+ # This will create a table in the database named "object_table" and all i/o
19
+ # against this object (unless specified otherwise on a per-method basis) will
20
+ # go through it.
21
+ #
22
+ # Note that while all object keys are treated as strings, values are
23
+ # marshalled and must be capable of doing so.
24
+ #
25
+ class Object < Generic
26
+ #
27
+ # Get an object by referencing its key. Returns nil unless it exists.
28
+ #
29
+ def [](key)
30
+ value = @db.execute("select value from #{@table_name} where key=?", [key]).first.first rescue nil
31
+ return value && Marshal.load(value)
32
+ end
33
+
34
+ #
35
+ # Set an object. Returns the object. The object must be able to be Marshalled.
36
+ #
37
+ def []=(key, value)
38
+ delete(key)
39
+ @db.execute("insert into #{@table_name} (key, value) values (?, ?)", [key, Marshal.dump(value)])
40
+ value
41
+ end
42
+
43
+ #
44
+ # Deletes an object.
45
+ #
46
+ def delete(key)
47
+ @db.execute("delete from #{@table_name} where key=?", [key])
48
+ end
49
+
50
+ #
51
+ # Defines the schema for this data type.
52
+ #
53
+ def create_table
54
+ @db.execute <<-EOF
55
+ create table if not exists #{@table_name} (
56
+ id integer not null primary key autoincrement,
57
+ key varchar(255) not null,
58
+ value text not null,
59
+ UNIQUE(key)
60
+ )
61
+ EOF
62
+ end
63
+ end
64
+ end