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 CHANGED
@@ -1,10 +1,11 @@
1
- lib/active_record_tuple.rb
2
- lib/data_mapper_tuple.rb
3
- lib/sequel_tuple.rb
4
- lib/snip.rb
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/soup_test.rb
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
- s = Snip.new
10
- s.name = "James"
11
- s.skills = "Bowstaff, nunchuck"
12
- s.save
9
+
10
+ Soup << {
11
+ :name => "James",
12
+ :skills => "Bowstaff, nunchuck"
13
+ }
13
14
 
14
15
  # ...much later...
15
16
 
16
- s = Snip['james']
17
+ s = Soup['james']
17
18
  s.skills # => "Bowstaff, nunchuck"
18
19
 
19
- x = Snip.new
20
- x.mane = "Lush and thick"
21
- x.teeth = "Sharp and ready"
22
- x.position = "Above my bed!!!"
23
- x.save
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 Snip, and it will be persisted without care.
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 - Snip doesn't really care much about the underlying
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!
@@ -1,7 +1,10 @@
1
- require 'snip'
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.1.5"
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.all_for_snip_named(name)
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.with(:created_at, "> '2007-01-01'")
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.with(name, tuple_value_conditions=nil)
39
- matching_tuples = Soup.tuple_class.find_matching(name, tuple_value_conditions)
40
- matching_tuples.map { |t| t.snip_id }.uniq.map { |snip_id| find(snip_id) }
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
@@ -1,26 +1,19 @@
1
1
 
2
- # Gem::Specification for Soup-0.1.5
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.1.5"
8
-
9
- s.specification_version = 2 if s.respond_to? :specification_version=
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.rubygems_version = %q{0.9.5}
22
- s.summary = %q{Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,}
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
- # # We have to run our own spec runner, because Snip will try to undefine
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]
@@ -1,46 +1,102 @@
1
- Soup.base = {:database => "soup_test.db"}
2
- Soup.prepare
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
3
2
 
4
- describe Snip, "when newly created" do
5
- before(:each) { @snip = Snip.new }
6
-
7
- it "should not have a name" do
8
- @snip.name.should be_nil
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
- it "should return nil for any attributes" do
12
- @snip.other_attribute.should be_nil
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
- it "should allow setting attributes" do
21
- @snip.something = "blah"
22
- @snip.something.should == "blah"
23
- end
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
- it "should not respond to attributes that have not been set" do
26
- @snip.should_not respond_to(:monkey)
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
- it "should respond to attributes that have been set" do
30
- @snip.monkey = true
31
- @snip.should respond_to(:monkey)
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 Snip, "when saving" do
36
- before(:each) { @snip = Snip.new }
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
- it "should return all attributes when reloading" do
39
- @snip.name = "something"
40
- @snip.jazz = "smooth"
41
- @snip.save
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
- other_snip = Snip['something']
44
- other_snip.jazz.should == "smooth"
98
+ other_snip = Soup['something_else']
99
+ other_snip.id.should == 100
100
+ end
45
101
  end
46
102
  end
@@ -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
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[.. lib]))
2
+
3
+ require 'soup'
4
+ require "soup/tuples/active_record_tuple"
5
+
6
+ def clear_database
7
+ Soup::Tuples::ActiveRecordTuple.destroy_all
8
+ end
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.1.5
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/active_record_tuple.rb
35
- - lib/data_mapper_tuple.rb
36
- - lib/sequel_tuple.rb
37
- - lib/snip.rb
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/soup_test.rb
41
+ - spec/soup_spec.rb
42
+ - spec/spec_helper.rb
42
43
  - spec/spec_runner.rb
43
- - Manifest
44
44
  - soup.gemspec
45
- has_rdoc: true
46
- homepage: ""
47
- post_install_message:
45
+ test_files: []
46
+
48
47
  rdoc_options: []
49
48
 
50
- require_paths:
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
- rubyforge_project: soup
67
- rubygems_version: 0.9.5
68
- signing_key:
69
- specification_version: 2
70
- summary: Soup is a bit of everything, summoned from nothing. Soup is like an imaginary friend - comforting,
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:
@@ -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
@@ -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
@@ -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
Binary file