soup 0.9.9.2 → 0.9.10

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