palsy 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/.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