soup 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +7 -6
- data/README +15 -13
- data/lib/soup.rb +57 -7
- data/lib/{snip.rb → soup/snip.rb} +35 -37
- data/lib/soup/tuples/active_record_tuple.rb +48 -0
- data/lib/soup/tuples/data_mapper_tuple.rb +50 -0
- data/lib/soup/tuples/sequel_tuple.rb +36 -0
- data/soup.gemspec +33 -23
- data/spec/snip_spec.rb +87 -31
- data/spec/soup_spec.rb +148 -0
- data/spec/spec_helper.rb +8 -0
- metadata +50 -56
- data/lib/active_record_tuple.rb +0 -49
- data/lib/data_mapper_tuple.rb +0 -43
- data/lib/sequel_tuple.rb +0 -29
- data/spec/soup_test.rb +0 -0
data/Manifest
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
lib/
|
2
|
-
lib/
|
3
|
-
lib/
|
4
|
-
lib/
|
1
|
+
lib/soup/snip.rb
|
2
|
+
lib/soup/tuples/active_record_tuple.rb
|
3
|
+
lib/soup/tuples/data_mapper_tuple.rb
|
4
|
+
lib/soup/tuples/sequel_tuple.rb
|
5
5
|
lib/soup.rb
|
6
|
+
Manifest
|
6
7
|
README
|
7
8
|
spec/snip_spec.rb
|
8
|
-
spec/
|
9
|
+
spec/soup_spec.rb
|
10
|
+
spec/spec_helper.rb
|
9
11
|
spec/spec_runner.rb
|
10
|
-
Manifest
|
data/README
CHANGED
@@ -6,26 +6,28 @@ Terrifying. And so:
|
|
6
6
|
require 'soup'
|
7
7
|
Soup.prepare
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
|
10
|
+
Soup << {
|
11
|
+
:name => "James",
|
12
|
+
:skills => "Bowstaff, nunchuck"
|
13
|
+
}
|
13
14
|
|
14
15
|
# ...much later...
|
15
16
|
|
16
|
-
s =
|
17
|
+
s = Soup['james']
|
17
18
|
s.skills # => "Bowstaff, nunchuck"
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
Soup << {
|
21
|
+
:mane => "Lush and thick"
|
22
|
+
:teeth => "Sharp and ready"
|
23
|
+
:position => "Above my bed!!!"
|
24
|
+
}
|
25
|
+
|
24
26
|
|
25
|
-
The point is that you can set any attribute on a
|
26
|
-
With reckless abandon, really.
|
27
|
+
The point is that you can set any attribute on a Soup data, and it will be persisted without
|
28
|
+
care. With reckless abandon, really.
|
27
29
|
|
28
|
-
The data can be stored using anything -
|
30
|
+
The data can be stored using anything - Soup doesn't really care much about the underlying
|
29
31
|
persistence layer. I've written implementations using DataMapper, ActiveRecord and Sequel...
|
30
32
|
there are other implementations of course. Unknowable implementations.
|
31
33
|
Terrifying implementations. You Fool! Warren is Dead!
|
data/lib/soup.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
require
|
1
|
+
# Let us require stuff in lib without saying lib/ all the time
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__)).uniq!
|
3
|
+
|
4
|
+
require 'soup/snip'
|
2
5
|
|
3
6
|
module Soup
|
4
|
-
VERSION = "0.
|
7
|
+
VERSION = "0.2.0"
|
5
8
|
|
6
9
|
DEFAULT_CONFIG = {
|
7
10
|
:adapter => 'sqlite3',
|
@@ -25,22 +28,69 @@ module Soup
|
|
25
28
|
#
|
26
29
|
def self.flavour=(tuple_implementation)
|
27
30
|
@tuple_implementation = "#{tuple_implementation}_tuple"
|
31
|
+
# We want to reset the tuple class if we re-flavour the soup.
|
32
|
+
@tuple_class = nil
|
28
33
|
end
|
29
34
|
|
30
35
|
def self.tuple_class
|
31
|
-
@tuple_class ||= case @tuple_implementation
|
36
|
+
@tuple_class ||= case (@tuple_implementation || DEFAULT_TUPLE_IMPLEMENTATION)
|
32
37
|
when "active_record_tuple", nil
|
33
|
-
ActiveRecordTuple
|
38
|
+
Soup::Tuples::ActiveRecordTuple
|
34
39
|
when "data_mapper_tuple"
|
35
|
-
DataMapperTuple
|
40
|
+
Soup::Tuples::DataMapperTuple
|
36
41
|
when "sequel_tuple"
|
37
|
-
SequelTuple
|
42
|
+
Soup::Tuples::SequelTuple
|
38
43
|
end
|
39
44
|
end
|
40
45
|
|
41
46
|
# Get the soup ready!
|
42
47
|
def self.prepare
|
43
|
-
require @tuple_implementation || DEFAULT_TUPLE_IMPLEMENTATION
|
48
|
+
require "soup/tuples/#{@tuple_implementation || DEFAULT_TUPLE_IMPLEMENTATION}"
|
44
49
|
tuple_class.prepare_database(DEFAULT_CONFIG.merge(@database_config || {}))
|
45
50
|
end
|
51
|
+
|
52
|
+
# The main interface
|
53
|
+
# ==================
|
54
|
+
|
55
|
+
# Finds bits in the soup that make the given attribute hash.
|
56
|
+
# This method should eventually be delegated to the underlying persistence
|
57
|
+
# layers (i.e. Snips and Tuples, or another document database). The expected
|
58
|
+
# behaviour is
|
59
|
+
def self.sieve(*args)
|
60
|
+
Snip.sieve(*args)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Puts some data into the soup, and returns an object that contains
|
64
|
+
# that data. The object should respond to accessing and setting its
|
65
|
+
# attributes as if they were defined using attr_accessor on the object's
|
66
|
+
# class.
|
67
|
+
def self.<<(attributes)
|
68
|
+
s = Snip.new(attributes)
|
69
|
+
s.save
|
70
|
+
s
|
71
|
+
end
|
72
|
+
|
73
|
+
# A shortcut to sieve by name attribute only
|
74
|
+
def self.[](*args)
|
75
|
+
results = if args[0].is_a?(Hash) || args.length > 1
|
76
|
+
sieve(*args)
|
77
|
+
else
|
78
|
+
sieve(:name => args[0])
|
79
|
+
end
|
80
|
+
results.length == 1 ? results.first : results
|
81
|
+
end
|
82
|
+
|
83
|
+
# ==== (interface ends) =====
|
84
|
+
|
85
|
+
# Save the current state of the soup into a YAML file.
|
86
|
+
def self.preserve(filename='soup.yml')
|
87
|
+
snips = {}
|
88
|
+
1.upto(Soup.tuple_class.next_snip_id) do |id|
|
89
|
+
snip = Snip.find(id) rescue nil
|
90
|
+
snips[snip.id] = snip if snip
|
91
|
+
end
|
92
|
+
File.open(filename, 'w') do |f|
|
93
|
+
f.puts snips.to_yaml
|
94
|
+
end
|
95
|
+
end
|
46
96
|
end
|
@@ -1,43 +1,37 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
|
3
|
-
# Based on Builder's BlankSlate object
|
4
|
-
class EmptyClass
|
5
|
-
instance_methods.each { |m| undef_method(m) unless m =~ /^(__|instance_eval|respond_to\?)/ }
|
6
|
-
end
|
2
|
+
require 'soup/empty_class'
|
7
3
|
|
8
4
|
# methods called on Tuple:
|
9
5
|
# Tuple.for_snip(id)
|
10
6
|
# Tuple.find_matching(tuple_name, tuple_value_conditions)
|
11
|
-
# Tuple.
|
7
|
+
# Tuple.find_matching_hash(key => value, key2 => value2, ...)
|
12
8
|
# Tuple.next_snip_id
|
13
9
|
# Tuple#save
|
14
10
|
# Tuple#name
|
15
11
|
# Tuple#value
|
16
12
|
# Tuple#destroy
|
17
13
|
|
18
|
-
class Snip < EmptyClass
|
19
|
-
|
20
|
-
# Returns the snip with the given name (i.e. the snip with the tuple of "name" -> name)
|
21
|
-
#
|
22
|
-
def self.[](name)
|
23
|
-
tuples = Soup.tuple_class.all_for_snip_named(name)
|
24
|
-
snip = Snip.new(:__id => tuples.first.snip_id)
|
25
|
-
snip.replace_tuples(tuples)
|
26
|
-
snip
|
27
|
-
rescue
|
28
|
-
return nil
|
29
|
-
end
|
14
|
+
class Snip < Soup::EmptyClass
|
30
15
|
|
31
16
|
# Returns all snips which match the given criteria, i.e. which have a tuple that
|
32
17
|
# matches the given conditions. For example:
|
33
18
|
#
|
34
|
-
# Snip.
|
19
|
+
# Snip.sieve(:created_at, "> '2007-01-01'")
|
35
20
|
#
|
36
21
|
# should return all Snips who have a 'created_at' value greater than '2007-01-01'.
|
37
22
|
#
|
38
|
-
def self.
|
39
|
-
|
40
|
-
|
23
|
+
def self.sieve(*args)
|
24
|
+
if args.length > 1
|
25
|
+
name = args[0]
|
26
|
+
tuple_value_conditions = args[1]
|
27
|
+
matching_tuples = Soup.tuple_class.find_matching(name, tuple_value_conditions)
|
28
|
+
matching_tuples.map { |t| t.snip_id }.uniq.map { |snip_id| find(snip_id) }
|
29
|
+
else
|
30
|
+
pairs = args[0].inject([]) { |ary, (name, value)| ary << [name, value] }
|
31
|
+
matching_tuples = pairs.map { |(name, value)| Soup.tuple_class.find_matching(name, "='#{value}'") }.flatten
|
32
|
+
snips = matching_tuples.map { |t| t.snip_id }.uniq.map { |snip_id| find(snip_id) }
|
33
|
+
snips.reject { |s| pairs.map { |(name, value)| s.get_value(name) == value }.include?(false) }
|
34
|
+
end
|
41
35
|
end
|
42
36
|
|
43
37
|
# Returns the snip with the given ID (i.e. the collection of all tuples
|
@@ -96,6 +90,10 @@ class Snip < EmptyClass
|
|
96
90
|
"<Snip id:#{self.id || "unset"} name:#{self.name}>"
|
97
91
|
end
|
98
92
|
|
93
|
+
def to_yaml(*args)
|
94
|
+
attributes.to_yaml(*args)
|
95
|
+
end
|
96
|
+
|
99
97
|
def method_missing(method, *args)
|
100
98
|
value = args.length > 1 ? args : args.first
|
101
99
|
if method.to_s =~ /(.*)=\Z/ # || value - could be a nice DSL touch.
|
@@ -109,6 +107,21 @@ class Snip < EmptyClass
|
|
109
107
|
@id
|
110
108
|
end
|
111
109
|
|
110
|
+
def get_value(name)
|
111
|
+
@tuples[name.to_s] ? @tuples[name.to_s].value : nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_value(name, value)
|
115
|
+
tuple = @tuples[name.to_s]
|
116
|
+
if tuple
|
117
|
+
tuple.value = value
|
118
|
+
else
|
119
|
+
attributes = {:snip_id => self.id, :name => name.to_s, :value => value}
|
120
|
+
tuple = @tuples[name.to_s] = Soup.tuple_class.new(attributes)
|
121
|
+
end
|
122
|
+
tuple.value
|
123
|
+
end
|
124
|
+
|
112
125
|
|
113
126
|
private
|
114
127
|
|
@@ -132,19 +145,4 @@ class Snip < EmptyClass
|
|
132
145
|
@tuples.inject("") { |hash, (name, tuple)| hash += " #{name}:'#{tuple.value}'" }.strip
|
133
146
|
end
|
134
147
|
|
135
|
-
def get_value(name)
|
136
|
-
@tuples[name.to_s] ? @tuples[name.to_s].value : nil
|
137
|
-
end
|
138
|
-
|
139
|
-
def set_value(name, value)
|
140
|
-
tuple = @tuples[name.to_s]
|
141
|
-
if tuple
|
142
|
-
tuple.value = value
|
143
|
-
else
|
144
|
-
attributes = {:snip_id => self.id, :name => name.to_s, :value => value}
|
145
|
-
tuple = @tuples[name.to_s] = Soup.tuple_class.new(attributes)
|
146
|
-
end
|
147
|
-
tuple.value
|
148
|
-
end
|
149
|
-
|
150
148
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'activerecord', '>=2.0.2'
|
3
|
+
require 'activerecord'
|
4
|
+
|
5
|
+
module Soup
|
6
|
+
module Tuples
|
7
|
+
class ActiveRecordTuple < ActiveRecord::Base
|
8
|
+
set_table_name :tuples
|
9
|
+
|
10
|
+
def self.prepare_database(config)
|
11
|
+
ActiveRecord::Base.establish_connection(config)
|
12
|
+
return if connection.tables.include?("tuples") # NOTE - this probably isn't good enough (what if the schema has changed?)
|
13
|
+
ActiveRecord::Migration.create_table :tuples, :force => true do |t|
|
14
|
+
t.column :snip_id, :integer
|
15
|
+
t.column :name, :string
|
16
|
+
t.column :value, :text
|
17
|
+
t.column :created_at, :datetime
|
18
|
+
t.column :updated_at, :datetime
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.for_snip(id)
|
23
|
+
find_all_by_snip_id(id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.find_matching(name, value_conditions=nil)
|
27
|
+
condition_sql = "name = '#{name}'"
|
28
|
+
condition_sql += " AND value #{value_conditions}" if value_conditions
|
29
|
+
find(:all, :conditions => condition_sql)
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO: *totally* not threadsafe.
|
33
|
+
def self.next_snip_id
|
34
|
+
maximum(:snip_id) + 1 rescue 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def save
|
38
|
+
super if new_record? || dirty?
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def dirty?
|
44
|
+
true # hmm.
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'datamapper'
|
3
|
+
require 'data_mapper'
|
4
|
+
|
5
|
+
module Soup
|
6
|
+
module Tuples
|
7
|
+
|
8
|
+
# This tuple implementation is broken - there's a weird interaction
|
9
|
+
# where values are not set within the web application.
|
10
|
+
#
|
11
|
+
class DataMapperTuple < DataMapper::Base
|
12
|
+
def self.prepare_database(config)
|
13
|
+
DataMapper::Database.setup(config)
|
14
|
+
# NOTE: so um, this property stuff doesn't like it if you're not connected to the db
|
15
|
+
# lets only have it once we are? Seems mental.
|
16
|
+
self.class_eval {
|
17
|
+
set_table_name 'tuples'
|
18
|
+
|
19
|
+
property :snip_id, :integer
|
20
|
+
|
21
|
+
property :name, :string
|
22
|
+
property :value, :text
|
23
|
+
|
24
|
+
property :created_at, :datetime
|
25
|
+
property :updated_at, :datetime
|
26
|
+
}
|
27
|
+
return if self.table.exists? # NOTE - this probably isn't good enough (what if the schema has changed?)
|
28
|
+
DataMapper::Persistence.auto_migrate!
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.for_snip(id)
|
32
|
+
all(:snip_id => id)
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: *totally* not threadsafe.
|
36
|
+
def self.next_snip_id
|
37
|
+
database.query("SELECT MAX(snip_id) + 1 FROM tuples")[0] || 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def save
|
41
|
+
if dirty? or new_record?
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :destroy, :destroy!
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'sequel'
|
3
|
+
require 'sequel'
|
4
|
+
|
5
|
+
DB = Sequel.sqlite 'soup_development.db'
|
6
|
+
|
7
|
+
module Soup
|
8
|
+
module Tuples
|
9
|
+
|
10
|
+
class SequelTuple < Sequel::Model(:tuples)
|
11
|
+
set_schema do
|
12
|
+
primary_key :id
|
13
|
+
integer :snip_id
|
14
|
+
string :name
|
15
|
+
string :value
|
16
|
+
datetime :created_at
|
17
|
+
datetime :updated_at
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.prepare_database(config)
|
21
|
+
# ummm... how to connect?
|
22
|
+
create_table # TODO: detect if the table already exists
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.for_snip(id)
|
26
|
+
filter(:snip_id => id).to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: *totally* not threadsafe.
|
30
|
+
def self.next_snip_id
|
31
|
+
max(:snip_id) + 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/soup.gemspec
CHANGED
@@ -1,26 +1,19 @@
|
|
1
1
|
|
2
|
-
# Gem::Specification for Soup-0.
|
2
|
+
# Gem::Specification for Soup-0.2.0
|
3
3
|
# Originally generated by Echoe
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = %q{soup}
|
7
|
-
s.version = "0.
|
8
|
-
|
9
|
-
s.
|
10
|
-
|
11
|
-
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
-
s.authors = ["James Adam"]
|
13
|
-
s.date = %q{2008-03-17}
|
14
|
-
s.description = %q{Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,}
|
7
|
+
s.version = "0.2.0"
|
8
|
+
s.date = %q{2008-04-17}
|
9
|
+
s.summary = %q{Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,}
|
15
10
|
s.email = ["james@lazyatom.com"]
|
16
|
-
s.files = ["lib/active_record_tuple.rb", "lib/data_mapper_tuple.rb", "lib/sequel_tuple.rb", "lib/snip.rb", "lib/soup.rb", "README", "spec/snip_spec.rb", "spec/soup_test.rb", "spec/spec_runner.rb", "Manifest", "soup.gemspec"]
|
17
|
-
s.has_rdoc = true
|
18
11
|
s.homepage = %q{}
|
19
|
-
s.require_paths = ["lib"]
|
20
12
|
s.rubyforge_project = %q{soup}
|
21
|
-
s.
|
22
|
-
s.
|
23
|
-
|
13
|
+
s.description = %q{Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,}
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.authors = ["James Adam"]
|
16
|
+
s.files = ["lib/soup/snip.rb", "lib/soup/tuples/active_record_tuple.rb", "lib/soup/tuples/data_mapper_tuple.rb", "lib/soup/tuples/sequel_tuple.rb", "lib/soup.rb", "Manifest", "README", "spec/snip_spec.rb", "spec/soup_spec.rb", "spec/spec_helper.rb", "spec/spec_runner.rb", "soup.gemspec"]
|
24
17
|
s.add_dependency(%q<activerecord>, [">= 2.0.2"])
|
25
18
|
end
|
26
19
|
|
@@ -39,20 +32,37 @@ end
|
|
39
32
|
# soup.email = ["james@lazyatom.com"]
|
40
33
|
# soup.description = File.readlines("README").first
|
41
34
|
# soup.dependencies = ["activerecord >=2.0.2"]
|
35
|
+
# soup.clean_pattern = ["*.db", "meta", "pkg"]
|
36
|
+
# soup.ignore_pattern = [".git", "*.db", "meta"]
|
42
37
|
# end
|
43
|
-
#
|
38
|
+
#
|
44
39
|
# rescue LoadError
|
45
40
|
# puts "You need to install the echoe gem to perform meta operations on this gem"
|
46
41
|
# end
|
47
42
|
#
|
43
|
+
# begin
|
44
|
+
# require 'spec'
|
45
|
+
# require 'spec/rake/spectask'
|
46
|
+
#
|
47
|
+
# Spec::Rake::SpecTask.new do |t|
|
48
|
+
# t.spec_opts = ["--format", "specdoc", "--colour"]
|
49
|
+
# t.spec_files = Dir['spec/**/*_spec.rb'].sort
|
50
|
+
# t.libs = ['lib']
|
51
|
+
# #t.rcov = true
|
52
|
+
# #t.rcov_dir = 'meta/coverage'
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# task :show_rcov do
|
56
|
+
# system 'open meta/coverage/index.html' if PLATFORM['darwin']
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# rescue LoadError
|
60
|
+
# puts "You need RSpec installed to run the spec (default) task on this gem"
|
61
|
+
# end
|
62
|
+
#
|
48
63
|
# desc "Open an irb session preloaded with this library"
|
49
64
|
# task :console do
|
50
|
-
# sh "irb -rubygems -r ./lib/soup.rb"
|
65
|
+
# sh "irb --prompt simple -rubygems -r ./lib/soup.rb"
|
51
66
|
# end
|
52
67
|
#
|
53
|
-
#
|
54
|
-
# # rspec's should methods using the default one
|
55
|
-
# task(:test) do
|
56
|
-
# files = FileList['spec/**/*_spec.rb']
|
57
|
-
# system "ruby spec/spec_runner.rb #{files} --format specdoc"
|
58
|
-
# end
|
68
|
+
# task :default => [:spec, :show_rcov]
|
data/spec/snip_spec.rb
CHANGED
@@ -1,46 +1,102 @@
|
|
1
|
-
|
2
|
-
Soup.prepare
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper")
|
3
2
|
|
4
|
-
describe Snip
|
5
|
-
before(:each)
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
describe Snip do
|
4
|
+
before(:each) do
|
5
|
+
Soup.base = {:database => "soup_test.db"}
|
6
|
+
Soup.prepare
|
7
|
+
clear_database
|
9
8
|
end
|
10
9
|
|
11
|
-
|
12
|
-
@snip.
|
10
|
+
describe "when newly created" do
|
11
|
+
before(:each) { @snip = Snip.new }
|
12
|
+
|
13
|
+
it "should not have a name" do
|
14
|
+
@snip.name.should be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return nil for any attributes" do
|
18
|
+
@snip.other_attribute.should be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should not have an id yet" do
|
22
|
+
@snip.id.should be_nil
|
23
|
+
end
|
13
24
|
end
|
14
|
-
end
|
15
25
|
|
16
|
-
|
17
|
-
describe Snip, "when setting attributes" do
|
18
|
-
before(:each) { @snip = Snip.new }
|
26
|
+
describe "when being created with attributes" do
|
19
27
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
it "should set attributes as passed in" do
|
29
|
+
@snip = Snip.new(:beats => 'phat', :rhymes => 100)
|
30
|
+
@snip.beats.should == 'phat'
|
31
|
+
@snip.rhymes.should == 100
|
32
|
+
end
|
24
33
|
|
25
|
-
|
26
|
-
|
34
|
+
it "should ignore any id passed in" do
|
35
|
+
@snip = Snip.new(:id => 1000)
|
36
|
+
@snip.id.should be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not ignore the secret __id" do
|
40
|
+
@snip = Snip.new(:__id => 1000)
|
41
|
+
@snip.id.should == 1000
|
42
|
+
end
|
27
43
|
end
|
44
|
+
|
45
|
+
describe "when setting attributes" do
|
46
|
+
before(:each) { @snip = Snip.new }
|
28
47
|
|
29
|
-
|
30
|
-
|
31
|
-
|
48
|
+
it "should allow setting attributes" do
|
49
|
+
@snip.something = "blah"
|
50
|
+
@snip.something.should == "blah"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should not respond to attributes that have not been set" do
|
54
|
+
@snip.should_not respond_to(:monkey)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should respond to attributes that have been set" do
|
58
|
+
@snip.monkey = true
|
59
|
+
@snip.should respond_to(:monkey)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should not allow setting of the id" do
|
63
|
+
@snip.id = 100
|
64
|
+
@snip.id.should_not == 100
|
65
|
+
@snip.id.should be_nil
|
66
|
+
end
|
32
67
|
end
|
33
|
-
end
|
34
68
|
|
35
|
-
describe
|
36
|
-
|
69
|
+
describe "when saving" do
|
70
|
+
before(:each) { @snip = Snip.new }
|
71
|
+
|
72
|
+
it "should not save if there's no data" do
|
73
|
+
lambda { @snip.save }.should raise_error
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should return all attributes when reloading" do
|
77
|
+
@snip.name = "something"
|
78
|
+
@snip.jazz = "smooth"
|
79
|
+
@snip.save
|
80
|
+
|
81
|
+
other_snip = Soup['something']
|
82
|
+
other_snip.jazz.should == "smooth"
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should generate an id" do
|
86
|
+
@snip.name = "something"
|
87
|
+
@snip.save
|
88
|
+
|
89
|
+
other_snip = Soup['something']
|
90
|
+
other_snip.id.should_not be_nil
|
91
|
+
end
|
37
92
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
93
|
+
it "should not overwrite an existing id created via __id" do
|
94
|
+
@snip = Snip.new(:__id => 100)
|
95
|
+
@snip.name = "something_else"
|
96
|
+
@snip.save
|
42
97
|
|
43
|
-
|
44
|
-
|
98
|
+
other_snip = Soup['something_else']
|
99
|
+
other_snip.id.should == 100
|
100
|
+
end
|
45
101
|
end
|
46
102
|
end
|
data/spec/soup_spec.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'soup'
|
2
|
+
|
3
|
+
describe Soup do
|
4
|
+
|
5
|
+
describe "when unflavoured or based" do
|
6
|
+
before(:each) { Soup.class_eval { @database_config = nil; @tuple_class = nil } }
|
7
|
+
it "should use the default database config" do
|
8
|
+
# I think this set of mock / expectations might be super wrong
|
9
|
+
Soup::DEFAULT_CONFIG.should_receive(:merge).with({}).and_return(Soup::DEFAULT_CONFIG)
|
10
|
+
Soup.tuple_class.should_receive(:prepare_database).with(Soup::DEFAULT_CONFIG)
|
11
|
+
Soup.prepare
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should use the default tuple implementation" do
|
15
|
+
# No real idea how to mock the require, or use aught but Secret Knowledge that AR == Default
|
16
|
+
Soup.tuple_class.should == Soup::Tuples::ActiveRecordTuple
|
17
|
+
Soup::Tuples::ActiveRecordTuple.should_receive(:prepare_database)
|
18
|
+
Soup.prepare
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "when being based" do
|
24
|
+
before(:each) { Soup.class_eval { @database_config = nil; @tuple_class = nil } }
|
25
|
+
|
26
|
+
it "should allow the base of the soup to be set" do
|
27
|
+
Soup.should respond_to(:base=)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should use the new base when preparing the soup" do
|
31
|
+
bouillabaisse = {:database => 'fishy.db', :adapter => 'fishdb'}
|
32
|
+
Soup.base = bouillabaisse
|
33
|
+
Soup.tuple_class.should_receive(:prepare_database).with(bouillabaisse)
|
34
|
+
Soup.prepare
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should merge incomplete bases with the default" do
|
38
|
+
tasteless = {:database => 'water.db'}
|
39
|
+
Soup.base = tasteless
|
40
|
+
Soup.tuple_class.should_receive(:prepare_database).with(Soup::DEFAULT_CONFIG.merge(tasteless))
|
41
|
+
Soup.prepare
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should allow the base to be reset" do
|
45
|
+
bouillabaisse = {:database => 'fishy.db', :adapter => 'fishdb'}
|
46
|
+
Soup.base = bouillabaisse
|
47
|
+
Soup.tuple_class.should_receive(:prepare_database).once.with(bouillabaisse).ordered
|
48
|
+
Soup.prepare
|
49
|
+
|
50
|
+
gazpacho = {:database => 'tomato.db', :adapter => 'colddb'}
|
51
|
+
Soup.base = gazpacho
|
52
|
+
Soup.tuple_class.should_receive(:prepare_database).once.with(gazpacho).ordered
|
53
|
+
Soup.prepare
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should not allow old bases to interfere with new ones" do
|
57
|
+
bouillabaisse = {:database => 'fishy.db', :adapter => 'fishdb'}
|
58
|
+
Soup.base = bouillabaisse
|
59
|
+
Soup.tuple_class.should_receive(:prepare_database).once.with(bouillabaisse).ordered
|
60
|
+
Soup.prepare
|
61
|
+
|
62
|
+
tasteless = {:database => 'water.db'}
|
63
|
+
Soup.base = tasteless
|
64
|
+
Soup.tuple_class.should_receive(:prepare_database).once.with(Soup::DEFAULT_CONFIG.merge(tasteless)).ordered
|
65
|
+
Soup.tuple_class.should_not_receive(:prepare_database).with(bouillabaisse.merge(tasteless))
|
66
|
+
Soup.prepare
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "when being flavoured" do
|
71
|
+
before(:each) { Soup.class_eval { @database_config = nil; @tuple_class = nil } }
|
72
|
+
|
73
|
+
it "should allow the soup to be flavoured" do
|
74
|
+
Soup.should respond_to(:flavour=)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should determine the tuple class based on the flavour" do
|
78
|
+
require 'soup/tuples/data_mapper_tuple'
|
79
|
+
Soup.flavour = :data_mapper
|
80
|
+
Soup.tuple_class.should == Soup::Tuples::DataMapperTuple
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should allow the flavour to be set multiple times" do
|
84
|
+
require 'soup/tuples/data_mapper_tuple'
|
85
|
+
Soup.flavour = :data_mapper
|
86
|
+
Soup.tuple_class.should == Soup::Tuples::DataMapperTuple
|
87
|
+
|
88
|
+
require 'soup/tuples/sequel_tuple'
|
89
|
+
Soup.flavour = :sequel
|
90
|
+
Soup.tuple_class.should_not == Soup::Tuples::DataMapperTuple
|
91
|
+
Soup.tuple_class.should == Soup::Tuples::SequelTuple
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should use have no tuple class if the flavour is unknowable" do
|
95
|
+
Soup.flavour = :shoggoth
|
96
|
+
Soup.tuple_class.should == nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "when adding data to the Soup directly" do
|
101
|
+
before(:each) do
|
102
|
+
Soup.base = {:database => "soup_test.db"}
|
103
|
+
Soup.flavour = :active_record
|
104
|
+
Soup.prepare
|
105
|
+
clear_database
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should create a new snip" do
|
109
|
+
attributes = {:name => 'monkey'}
|
110
|
+
Snip.should_receive(:new).with(attributes).and_return(mock('snip', :null_object => true))
|
111
|
+
Soup << attributes
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should save the snip" do
|
115
|
+
attributes = {:name => 'monkey'}
|
116
|
+
Snip.should_receive(:new).with(attributes).and_return(snip = mock('snip'))
|
117
|
+
snip.should_receive(:save)
|
118
|
+
Soup << attributes
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "when sieving the soup" do
|
123
|
+
before(:each) do
|
124
|
+
Soup.base = {:database => "soup_test.db"}
|
125
|
+
Soup.flavour = :active_record
|
126
|
+
Soup.prepare
|
127
|
+
clear_database
|
128
|
+
@james = Soup << {:name => 'james', :spirit_guide => 'fox', :colour => 'blue', :powers => 'yes'}
|
129
|
+
@murray = Soup << {:name => 'murray', :spirit_guide => 'chaffinch', :colour => 'red', :powers => 'yes'}
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should find snips by name if the parameter is a string" do
|
133
|
+
Soup['james'].should == @james
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should find snips using exact matching of keys and values if the parameter is a hash" do
|
137
|
+
Soup[:name => 'murray'].should == @murray
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should match using all parameters" do
|
141
|
+
Soup[:powers => 'yes', :colour => 'red'].should == @james
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should return an array if more than one snip matches" do
|
145
|
+
Soup[:powers => 'yes'].should == [@james, @murray]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,72 +1,66 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
2
4
|
name: soup
|
3
5
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2008-04-17 00:00:00 +01:00
|
8
|
+
summary: Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email:
|
12
|
+
- james@lazyatom.com
|
13
|
+
homepage: ""
|
14
|
+
rubyforge_project: soup
|
15
|
+
description: Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,
|
16
|
+
autorequire:
|
17
|
+
default_executable:
|
18
|
+
bindir: bin
|
19
|
+
has_rdoc: true
|
20
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">"
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.0.0
|
25
|
+
version:
|
5
26
|
platform: ruby
|
27
|
+
signing_key:
|
28
|
+
cert_chain:
|
29
|
+
post_install_message:
|
6
30
|
authors:
|
7
31
|
- James Adam
|
8
|
-
autorequire:
|
9
|
-
bindir: bin
|
10
|
-
cert_chain: []
|
11
|
-
|
12
|
-
date: 2008-03-17 00:00:00 +00:00
|
13
|
-
default_executable:
|
14
|
-
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
16
|
-
name: activerecord
|
17
|
-
version_requirement:
|
18
|
-
version_requirements: !ruby/object:Gem::Requirement
|
19
|
-
requirements:
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 2.0.2
|
23
|
-
version:
|
24
|
-
description: Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,
|
25
|
-
email:
|
26
|
-
- james@lazyatom.com
|
27
|
-
executables: []
|
28
|
-
|
29
|
-
extensions: []
|
30
|
-
|
31
|
-
extra_rdoc_files: []
|
32
|
-
|
33
32
|
files:
|
34
|
-
- lib/
|
35
|
-
- lib/
|
36
|
-
- lib/
|
37
|
-
- lib/
|
33
|
+
- lib/soup/snip.rb
|
34
|
+
- lib/soup/tuples/active_record_tuple.rb
|
35
|
+
- lib/soup/tuples/data_mapper_tuple.rb
|
36
|
+
- lib/soup/tuples/sequel_tuple.rb
|
38
37
|
- lib/soup.rb
|
38
|
+
- Manifest
|
39
39
|
- README
|
40
40
|
- spec/snip_spec.rb
|
41
|
-
- spec/
|
41
|
+
- spec/soup_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
42
43
|
- spec/spec_runner.rb
|
43
|
-
- Manifest
|
44
44
|
- soup.gemspec
|
45
|
-
|
46
|
-
|
47
|
-
post_install_message:
|
45
|
+
test_files: []
|
46
|
+
|
48
47
|
rdoc_options: []
|
49
48
|
|
50
|
-
|
51
|
-
- lib
|
52
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
-
requirements:
|
54
|
-
- - ">="
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
version: "0"
|
57
|
-
version:
|
58
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
-
requirements:
|
60
|
-
- - ">="
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: "0"
|
63
|
-
version:
|
64
|
-
requirements: []
|
49
|
+
extra_rdoc_files: []
|
65
50
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
test_files: []
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
requirements: []
|
72
56
|
|
57
|
+
dependencies:
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: activerecord
|
60
|
+
version_requirement:
|
61
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.0.2
|
66
|
+
version:
|
data/lib/active_record_tuple.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
gem 'activerecord', '>=2.0.2'
|
3
|
-
require 'activerecord'
|
4
|
-
|
5
|
-
class ActiveRecordTuple < ActiveRecord::Base
|
6
|
-
set_table_name :tuples
|
7
|
-
|
8
|
-
def self.prepare_database(config)
|
9
|
-
ActiveRecord::Base.establish_connection(config)
|
10
|
-
return if connection.tables.include?("tuples")
|
11
|
-
ActiveRecord::Migration.create_table :tuples, :force => true do |t|
|
12
|
-
t.column :snip_id, :integer
|
13
|
-
t.column :name, :string
|
14
|
-
t.column :value, :text
|
15
|
-
t.column :created_at, :datetime
|
16
|
-
t.column :updated_at, :datetime
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.for_snip(id)
|
21
|
-
find_all_by_snip_id(id)
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.find_matching(name, value_conditions=nil)
|
25
|
-
condition_sql = "name = '#{name}'"
|
26
|
-
condition_sql += " AND value #{value_conditions}" if value_conditions
|
27
|
-
find(:all, :conditions => condition_sql)
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.all_for_snip_named(name)
|
31
|
-
id = find_by_name_and_value("name", name).snip_id
|
32
|
-
for_snip(id)
|
33
|
-
end
|
34
|
-
|
35
|
-
# TODO: *totally* not threadsafe.
|
36
|
-
def self.next_snip_id
|
37
|
-
maximum(:snip_id) + 1 rescue 1
|
38
|
-
end
|
39
|
-
|
40
|
-
def save
|
41
|
-
super if new_record? || dirty?
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
def dirty?
|
47
|
-
true # hmm.
|
48
|
-
end
|
49
|
-
end
|
data/lib/data_mapper_tuple.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
gem 'data_mapper'
|
3
|
-
|
4
|
-
# This tuple implementation is broken - there's a weird interaction
|
5
|
-
# where values are not set within the web application.
|
6
|
-
#
|
7
|
-
class DataMapperTuple < DataMapper::Base
|
8
|
-
|
9
|
-
property :snip_id, :integer
|
10
|
-
|
11
|
-
property :name, :string
|
12
|
-
property :value, :text
|
13
|
-
|
14
|
-
property :created_at, :datetime
|
15
|
-
property :updated_at, :datetime
|
16
|
-
|
17
|
-
def self.prepare_database(config)
|
18
|
-
DataMapper::Database.setup(config)
|
19
|
-
DataMapper::Persistence.auto_migrate! # TODO: detect if the table exists
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.for_snip(id)
|
23
|
-
all(:snip_id => id)
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.all_for_snip_named(name)
|
27
|
-
id = first(:name => "name", :value => name).snip_id
|
28
|
-
for_snip(id)
|
29
|
-
end
|
30
|
-
|
31
|
-
# TODO: *totally* not threadsafe.
|
32
|
-
def self.next_snip_id
|
33
|
-
database.query("SELECT MAX(snip_id) + 1 FROM tuples")[0] || 1
|
34
|
-
end
|
35
|
-
|
36
|
-
def save
|
37
|
-
if dirty? or new_record?
|
38
|
-
super
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
alias_method :destroy, :destroy!
|
43
|
-
end
|
data/lib/sequel_tuple.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
gem 'sequel'
|
3
|
-
|
4
|
-
DB = Sequel.sqlite 'soup_development.db'
|
5
|
-
|
6
|
-
class SequelTuple < Sequel::Model(:tuples)
|
7
|
-
set_schema do
|
8
|
-
primary_key :id
|
9
|
-
integer :snip_id
|
10
|
-
string :name
|
11
|
-
string :value
|
12
|
-
datetime :created_at
|
13
|
-
datetime :updated_at
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.prepare_database(config)
|
17
|
-
# ummm... how to connect?
|
18
|
-
create_table # TODO: detect if the table already exists
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.for_snip(id)
|
22
|
-
filter(:snip_id => id).to_a
|
23
|
-
end
|
24
|
-
|
25
|
-
# TODO: *totally* not threadsafe.
|
26
|
-
def self.next_snip_id
|
27
|
-
max(:snip_id) + 1
|
28
|
-
end
|
29
|
-
end
|
data/spec/soup_test.rb
DELETED
Binary file
|