soup 0.9.9.2 → 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -4,26 +4,25 @@ and will often talk to you, but when you look closely, they don't exist.
4
4
  Terrifying. And so:
5
5
 
6
6
  require 'soup'
7
- Soup.prepare
7
+ soup = Soup.new(File.join("soup", __FILE__))
8
8
 
9
-
10
- Soup << {
9
+ soup << {
11
10
  :name => "James",
12
11
  :skills => "Bowstaff, nunchuck"
13
12
  }
14
-
13
+
15
14
  # ...much later...
16
-
17
- s = Soup['james']
15
+
16
+ s = soup['james']
18
17
  s.skills # => "Bowstaff, nunchuck"
19
-
20
- Soup << {
18
+
19
+ soup << {
21
20
  :mane => "Lush and thick"
22
21
  :teeth => "Sharp and ready"
23
22
  :position => "Above my bed!!!"
24
23
  }
25
24
 
26
-
25
+
27
26
  The point is that you can set any attribute on a Soup data, and it will be persisted without
28
27
  care. With reckless abandon, really.
29
28
 
data/Rakefile CHANGED
@@ -7,6 +7,7 @@ task :default => :test
7
7
  require "rake/testtask"
8
8
  Rake::TestTask.new do |t|
9
9
  t.libs << "test"
10
+ t.ruby_opts << "-rubygems"
10
11
  t.test_files = FileList["test/**/*_test.rb"]
11
12
  t.verbose = true
12
13
  end
@@ -20,7 +21,7 @@ spec = Gem::Specification.new do |s|
20
21
 
21
22
  # Change these as appropriate
22
23
  s.name = "soup"
23
- s.version = "0.9.9.2"
24
+ s.version = "0.9.10"
24
25
  s.summary = "A super-simple data store"
25
26
  s.author = "James Adam"
26
27
  s.email = "james@lazyatom.com"
@@ -40,25 +41,25 @@ spec = Gem::Specification.new do |s|
40
41
  # s.add_dependency("some_other_gem", "~> 0.1.0")
41
42
 
42
43
  # If your tests use any gems, include them here
43
- # s.add_development_dependency("mocha")
44
-
45
- # If you want to publish automatically to rubyforge, you'll may need
46
- # to tweak this, and the publishing task below too.
47
- s.rubyforge_project = "soup"
44
+ s.add_development_dependency("shoulda")
48
45
  end
49
46
 
50
- # This task actually builds the gem. We also regenerate a static
47
+ # This task actually builds the gem. We also regenerate a static
51
48
  # .gemspec file, which is useful if something (i.e. GitHub) will
52
49
  # be automatically building a gem for this project. If you're not
53
50
  # using GitHub, edit as appropriate.
54
51
  Rake::GemPackageTask.new(spec) do |pkg|
55
52
  pkg.gem_spec = spec
56
-
57
- # Generate the gemspec file for github.
53
+ end
54
+
55
+ desc "Build the gemspec file #{spec.name}.gemspec"
56
+ task :gemspec do
58
57
  file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
59
58
  File.open(file, "w") {|f| f << spec.to_ruby }
60
59
  end
61
60
 
61
+ task :package => :gemspec
62
+
62
63
  # Generate documentation
63
64
  Rake::RDocTask.new do |rd|
64
65
  rd.main = "README"
@@ -70,46 +71,3 @@ desc 'Clear out RDoc and generated packages'
70
71
  task :clean => [:clobber_rdoc, :clobber_package] do
71
72
  rm "#{spec.name}.gemspec"
72
73
  end
73
-
74
- # If you want to publish to RubyForge automatically, here's a simple
75
- # task to help do that. If you don't, just get rid of this.
76
- # Be sure to set up your Rubyforge account details with the Rubyforge
77
- # gem; you'll need to run `rubyforge setup` and `rubyforge config` at
78
- # the very least.
79
- begin
80
- require "rake/contrib/sshpublisher"
81
- namespace :rubyforge do
82
-
83
- desc "Release gem and RDoc documentation to RubyForge"
84
- task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
85
-
86
- namespace :release do
87
- desc "Release a new version of this gem"
88
- task :gem => [:package] do
89
- require 'rubyforge'
90
- rubyforge = RubyForge.new
91
- rubyforge.configure
92
- rubyforge.login
93
- rubyforge.userconfig['release_notes'] = spec.summary
94
- path_to_gem = File.join(File.dirname(__FILE__), "pkg", "#{spec.name}-#{spec.version}.gem")
95
- puts "Publishing #{spec.name}-#{spec.version.to_s} to Rubyforge..."
96
- rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, path_to_gem)
97
- end
98
-
99
- desc "Publish RDoc to RubyForge."
100
- task :docs => [:rdoc] do
101
- config = YAML.load(
102
- File.read(File.expand_path('~/.rubyforge/user-config.yml'))
103
- )
104
-
105
- host = "#{config['username']}@rubyforge.org"
106
- remote_dir = "/var/www/gforge-projects/soup/" # Should be the same as the rubyforge project name
107
- local_dir = 'rdoc'
108
-
109
- Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
110
- end
111
- end
112
- end
113
- rescue LoadError
114
- puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
115
- end
data/lib/soup.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  # Let us require stuff in lib without saying lib/ all the time
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__)).uniq!
3
3
 
4
- require 'soup/snip'
5
4
  require 'yaml'
6
5
  require 'fileutils'
7
6
 
8
7
  class Soup
9
- VERSION = "0.9.9"
8
+ autoload :Backends, 'soup/backends'
9
+ autoload :Snip, 'soup/snip'
10
10
 
11
11
  # You can access a default soup using this methods.
12
12
 
@@ -22,7 +22,7 @@ class Soup
22
22
  default_instance << attributes
23
23
  end
24
24
 
25
- def self.sieve(*args)
25
+ def self.with(*args)
26
26
  default_instance.sieve(*args)
27
27
  end
28
28
 
@@ -30,22 +30,20 @@ class Soup
30
30
  default_instance.destroy(*args)
31
31
  end
32
32
 
33
- attr_reader :base_path
34
-
35
33
  # Get the soup ready!
36
- def initialize(base_path=nil)
37
- @base_path = base_path || "soup"
38
- FileUtils.mkdir_p(base_path)
34
+ def initialize(backend=nil)
35
+ @backend = backend || Soup::Backends::YAMLBackend.new
36
+ @backend.prepare
39
37
  end
40
38
 
41
39
  # The main interface
42
40
  # ==================
43
41
 
44
- # A shorthand for #sieve, with the addition that only a name may be
45
- # supplied (i.e. Soup['my snip'])
42
+ # A shorthand for #with, with the addition that only a name may be
43
+ # supplied (i.e. soup['my snip'])
46
44
  def [](conditions)
47
45
  conditions = {:name => conditions} unless conditions.respond_to?(:keys)
48
- sieve(conditions)
46
+ with(conditions)
49
47
  end
50
48
 
51
49
  # Puts some data into the soup, and returns an object that contains
@@ -53,72 +51,23 @@ class Soup
53
51
  # attributes as if they were defined using attr_accessor on the object's
54
52
  # class.
55
53
  def <<(attributes)
56
- save_snip(attributes)
57
- Snip.new(attributes, self)
54
+ @backend.save_snip(symbolize_keys(attributes))
58
55
  end
59
56
 
60
57
  # Finds bits in the soup that make the given attribute hash.
61
58
  # This method should eventually be delegated to the underlying persistence
62
59
  # layers (i.e. Snips and Tuples, or another document database). The expected
63
60
  # behaviour is
64
- def sieve(conditions)
65
- conditions = symbolize_keys(conditions)
66
- if conditions.keys == [:name]
67
- load_snip(conditions[:name])
68
- else
69
- all_snips.select do |s|
70
- conditions.inject(true) do |matches, (key, value)|
71
- matches && (s.__send__(key) == value)
72
- end
73
- end
74
- end
61
+ def with(conditions)
62
+ @backend.find(symbolize_keys(conditions))
75
63
  end
76
64
 
77
65
  def destroy(name)
78
- File.delete(path_for(name))
79
- end
80
-
81
- def all_snips
82
- Dir[path_for("*")].map do |path|
83
- load_snip(File.basename(path, ".yml"))
84
- end
66
+ @backend.destroy(name)
85
67
  end
86
68
 
87
69
  private
88
70
 
89
- def save_snip(attributes)
90
- attributes = symbolize_keys(attributes)
91
- File.open(path_for(attributes[:name]), 'w') do |f|
92
- content = attributes.delete(:content)
93
- f.write content
94
- f.write attributes.to_yaml.gsub(/^---\s/, attribute_token) if attributes.any?
95
- end
96
- end
97
-
98
- def load_snip(name)
99
- path = path_for(name)
100
- if File.exist?(path)
101
- file = File.read(path)
102
- if attribute_start = file.index(attribute_token)
103
- content = file.slice(0...attribute_start)
104
- attributes = {:name => name}.merge(YAML.load(file.slice(attribute_start..-1)).merge(:content => content))
105
- else
106
- attributes = {:content => file, :name => name}
107
- end
108
- Snip.new(attributes, self)
109
- else
110
- nil
111
- end
112
- end
113
-
114
- def path_for(filename)
115
- File.join(base_path, filename + ".yml")
116
- end
117
-
118
- def attribute_token
119
- "--- # Soup attributes"
120
- end
121
-
122
71
  def symbolize_keys(hash)
123
72
  hash.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
124
73
  end
@@ -0,0 +1,13 @@
1
+ class Soup
2
+ # Backends should implement (or delegate) the following API:
3
+ # * #prepare - will be called when a Soup is created
4
+ # * #names - should return the names of all snips contained
5
+ # * #load_snip(name) - should return a Soup::Snip, or nil if it couldn't be loaded
6
+ # * #save_snip(attribute_hash) - should store and return a Soup::Snip, or nil if it couldn't be saved
7
+ # * #destroy(name) - should return true if the snip was removed, or false if otherwise
8
+ module Backends
9
+ autoload :YAMLBackend, 'soup/backends/yaml_backend'
10
+ autoload :MultiSoup, 'soup/backends/multi_soup'
11
+ autoload :ReadOnly, 'soup/backends/read_only'
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ class Soup
2
+ module Backends
3
+ class MultiSoup
4
+ def initialize(*backends)
5
+ @backends = backends
6
+ end
7
+
8
+ def prepare
9
+ @backends.each { |b| b.prepare }
10
+ end
11
+
12
+ def method_missing(*args)
13
+ @backends.each do |backend|
14
+ if result = backend.__send__(*args)
15
+ return result
16
+ end
17
+ end
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ class Soup
2
+ module Backends
3
+ class ReadOnly
4
+ def initialize(backend)
5
+ @backend = backend
6
+ end
7
+
8
+ def save_snip(*args)
9
+ false
10
+ end
11
+
12
+ def method_missing(*args)
13
+ @backend.__send__(*args)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,78 @@
1
+ class Soup
2
+ module Backends
3
+ class YAMLBackend
4
+ ATTRIBUTE_TOKEN = "--- # Soup attributes"
5
+
6
+ def initialize(path="soup")
7
+ @base_path = path
8
+ end
9
+
10
+ def prepare
11
+ FileUtils.mkdir_p(@base_path)
12
+ end
13
+
14
+ def names
15
+ Dir[path_for("*")].map { |s| File.basename(s, ".yml") }
16
+ end
17
+
18
+ def find(conditions)
19
+ if conditions.keys == [:name]
20
+ load_snip(conditions[:name])
21
+ else
22
+ all_snips.select do |s|
23
+ conditions.inject(true) do |matches, (key, value)|
24
+ matches && (s.__send__(key) == value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def load_snip(name)
31
+ path = path_for(name)
32
+ if File.exist?(path)
33
+ file = File.read(path)
34
+ if attribute_start = file.index(ATTRIBUTE_TOKEN)
35
+ content = file.slice(0...attribute_start)
36
+ attributes = {:name => name}.merge(YAML.load(file.slice(attribute_start..-1)).merge(:content => content))
37
+ else
38
+ attributes = {:content => file, :name => name}
39
+ end
40
+ Snip.new(attributes, self)
41
+ else
42
+ nil
43
+ end
44
+ end
45
+
46
+ def save_snip(attributes)
47
+ File.open(path_for(attributes[:name]), 'w') do |f|
48
+ content = attributes.delete(:content)
49
+ f.write content
50
+ f.write attributes.to_yaml.gsub(/^---\s/, ATTRIBUTE_TOKEN) if attributes.any?
51
+ end
52
+ Snip.new(attributes, self)
53
+ end
54
+
55
+ def destroy(name)
56
+ path = path_for(name)
57
+ if File.exist?(path)
58
+ File.delete(path)
59
+ true
60
+ else
61
+ nil
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def path_for(name)
68
+ File.join(@base_path, name + ".yml")
69
+ end
70
+
71
+ def all_snips
72
+ Dir[path_for("*")].map do |key|
73
+ load_snip(File.basename(key, ".yml"))
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
data/lib/soup/snip.rb CHANGED
@@ -1,38 +1,39 @@
1
1
  require 'soup/empty_class'
2
2
 
3
- class Snip < Soup::EmptyClass
4
- attr_reader :attributes
5
-
6
- def initialize(attributes = {}, soup = Soup)
7
- @attributes = attributes
8
- @soup = soup
9
- end
3
+ class Soup
4
+ class Snip < Soup::EmptyClass
5
+ attr_reader :attributes
10
6
 
11
- def save
12
- @soup << @attributes
13
- self
14
- end
7
+ def initialize(attributes, backend)
8
+ @attributes = attributes
9
+ @backend = backend
10
+ end
15
11
 
16
- def destroy
17
- @soup.destroy(self.name)
18
- self
19
- end
12
+ def save
13
+ @backend.save_snip(@attributes)
14
+ self
15
+ end
20
16
 
21
- def inspect
22
- "<Snip name:#{self.name}>"
23
- end
17
+ def destroy
18
+ @backend.destroy(self.name)
19
+ self
20
+ end
24
21
 
25
- def respond_to?(method)
26
- @attributes.keys.include?(method.to_s)
27
- end
22
+ def inspect
23
+ "<Snip name:#{self.name}>"
24
+ end
28
25
 
29
- def method_missing(method, *args)
30
- value = args.length > 1 ? args : args.first
31
- if method.to_s =~ /(.*)=\Z/
32
- @attributes[$1.to_sym] = value
33
- else
34
- @attributes[method]
26
+ def respond_to?(method)
27
+ @attributes.keys.include?(method.to_s)
35
28
  end
36
- end
37
29
 
30
+ def method_missing(method, *args)
31
+ value = args.length > 1 ? args : args.first
32
+ if method.to_s =~ /(.*)=\Z/
33
+ @attributes[$1.to_sym] = value
34
+ else
35
+ @attributes[method]
36
+ end
37
+ end
38
+ end
38
39
  end
@@ -0,0 +1,65 @@
1
+ require "test_helper"
2
+
3
+ class MultiSoupBackendTest < Test::Unit::TestCase
4
+ context "A Soup with multiple backends" do
5
+ setup do
6
+ @base_path = File.join(File.dirname(__FILE__), *%w[.. tmp soup])
7
+ @basic_soup_backend_one = Soup::Backends::YAMLBackend.new(File.join(@base_path, "soup_one"))
8
+ @basic_soup_backend_two = Soup::Backends::YAMLBackend.new(File.join(@base_path, "soup_two"))
9
+ @soup_one = Soup.new(@basic_soup_backend_one)
10
+ @soup_two = Soup.new(@basic_soup_backend_two)
11
+ multi_soup_backend = Soup::Backends::MultiSoup.new(@basic_soup_backend_one, @basic_soup_backend_two)
12
+ @soup = Soup.new(multi_soup_backend)
13
+ end
14
+
15
+ teardown do
16
+ FileUtils.rm_rf(@base_path)
17
+ end
18
+
19
+ should "return nil when the requested snip is not present in any backend" do
20
+ assert_nil @soup["snip"]
21
+ end
22
+
23
+ should "return a snip if any backend contains it" do
24
+ @soup_one << {:name => "snip", :body => "hello"}
25
+ assert_equal "hello", @soup["snip"].body
26
+
27
+ @soup_two << {:name => "other_snip", :body => "hi!"}
28
+ assert_equal "hi!", @soup["other_snip"].body
29
+ end
30
+
31
+ context "when snips of the same name exist in multiple backends" do
32
+ setup do
33
+ @soup_one << {:name => "snip", :body => "from soup one"}
34
+ @soup_two << {:name => "snip", :body => "from soup two"}
35
+ end
36
+
37
+ should "load the snip from the backend with the higher precidence" do
38
+ assert_equal "from soup one", @soup["snip"].body
39
+ end
40
+ end
41
+
42
+ should "save snips" do
43
+ @soup << {:name => "snip", :body => "bad snip"}
44
+ @soup.destroy("snip")
45
+ assert_nil @soup["snip"]
46
+ end
47
+
48
+ context "when a backend is read-only" do
49
+ setup do
50
+ readonly_backend = Soup::Backends::ReadOnly.new(@basic_soup_backend_one)
51
+ @soup_one = Soup.new(readonly_backend)
52
+ @soup_two = Soup.new(@basic_soup_backend_two)
53
+ multi_soup_backend = Soup::Backends::MultiSoup.new(readonly_backend, @basic_soup_backend_two)
54
+ @soup = Soup.new(multi_soup_backend)
55
+ end
56
+
57
+ should "store snips in the writeable backend" do
58
+ @soup << {:name => "snip", :body => "hello"}
59
+ assert_equal "hello", @soup["snip"].body
60
+ assert_nil @soup_one["snip"]
61
+ assert_not_nil @soup_two["snip"]
62
+ end
63
+ end
64
+ end
65
+ end
data/test/soup_test.rb CHANGED
@@ -1,14 +1,27 @@
1
- require 'test/unit'
2
- require 'shoulda'
3
- require 'soup'
1
+ require "test_helper"
4
2
 
5
3
  class SoupTest < Test::Unit::TestCase
6
4
 
7
- context "Given a soup" do
8
- setup do
9
- @soup = Soup.new(File.join(File.dirname(__FILE__), *%w[.. tmp soup]))
5
+ def self.each_backend(&block)
6
+ base_path = File.join(File.dirname(__FILE__), *%w[.. tmp soup])
7
+ backends = [
8
+ yaml_backend = Soup::Backends::YAMLBackend.new(base_path),
9
+ Soup::Backends::MultiSoup.new(yaml_backend)
10
+ ]
11
+ backends.each do |backend|
12
+ context "The #{backend.class.name} Soup backend" do
13
+ setup do
14
+ @soup = Soup.new(backend)
15
+ end
16
+ teardown do
17
+ FileUtils.rm_rf(base_path)
18
+ end
19
+ yield backend
20
+ end
10
21
  end
22
+ end
11
23
 
24
+ each_backend do |backend|
12
25
  should "be able to store content" do
13
26
  @soup << {:name => 'test', :content => "I like stuff, and things"}
14
27
  assert_equal "I like stuff, and things", @soup['test'].content
@@ -41,14 +54,12 @@ class SoupTest < Test::Unit::TestCase
41
54
  end
42
55
  end
43
56
 
44
- context "when deleting snips" do
45
- should "allow deletion of snips" do
46
- snip = @soup << {:name => 'test', :content => 'content'}
47
- assert_equal snip, @soup['test']
57
+ should "allow deletion of snips" do
58
+ snip = @soup << {:name => 'test', :content => 'content'}
59
+ assert_equal snip, @soup['test']
48
60
 
49
- @soup['test'].destroy
50
- assert @soup['test'].nil?
51
- end
61
+ @soup['test'].destroy
62
+ assert @soup['test'].nil?
52
63
  end
53
64
  end
54
65
  end
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require 'shoulda'
3
+ require 'soup'
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.9.2
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 10
9
+ version: 0.9.10
5
10
  platform: ruby
6
11
  authors:
7
12
  - James Adam
@@ -9,10 +14,21 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2009-12-16 00:00:00 +00:00
17
+ date: 2010-05-30 00:00:00 +02:00
13
18
  default_executable:
14
- dependencies: []
15
-
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: shoulda
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
16
32
  description:
17
33
  email: james@lazyatom.com
18
34
  executables: []
@@ -25,7 +41,13 @@ files:
25
41
  - Manifest
26
42
  - Rakefile
27
43
  - README
44
+ - test/multi_soup_backend_test.rb
28
45
  - test/soup_test.rb
46
+ - test/test_helper.rb
47
+ - lib/soup/backends/multi_soup.rb
48
+ - lib/soup/backends/read_only.rb
49
+ - lib/soup/backends/yaml_backend.rb
50
+ - lib/soup/backends.rb
29
51
  - lib/soup/empty_class.rb
30
52
  - lib/soup/snip.rb
31
53
  - lib/soup.rb
@@ -43,18 +65,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
65
  requirements:
44
66
  - - ">="
45
67
  - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
46
70
  version: "0"
47
- version:
48
71
  required_rubygems_version: !ruby/object:Gem::Requirement
49
72
  requirements:
50
73
  - - ">="
51
74
  - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
52
77
  version: "0"
53
- version:
54
78
  requirements: []
55
79
 
56
- rubyforge_project: soup
57
- rubygems_version: 1.3.5
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.6
58
82
  signing_key:
59
83
  specification_version: 3
60
84
  summary: A super-simple data store