soup 0.1.5 → 0.2.0
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/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
|