boombera 0.2.1 → 0.2.2

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.2.2
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{boombera}
8
- s.version = "0.2.1"
8
+ s.version = "0.2.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["John Wilger"]
12
- s.date = %q{2011-05-30}
12
+ s.date = %q{2011-05-31}
13
13
  s.description = %q{CouchDB-backed content repository for multi-tenant, multi-stage applications}
14
14
  s.email = %q{johnwilger@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -32,6 +32,7 @@ Gem::Specification.new do |s|
32
32
  "lib/boombera/content_item.rb",
33
33
  "lib/boombera/information.rb",
34
34
  "spec/integration/boombera_spec.rb",
35
+ "spec/lib/boombera/content_item_map_resolver_spec.rb",
35
36
  "spec/lib/boombera/content_item_spec.rb",
36
37
  "spec/lib/boombera_spec.rb",
37
38
  "spec/spec_helper.rb"
@@ -1,40 +1,147 @@
1
1
  $:.unshift(File.expand_path(File.dirname(__FILE__)))
2
2
  require 'couchrest'
3
3
 
4
+ # This is the main interface to the Boombera content repository.
5
+ #
6
+ # Usage examples:
7
+ #
8
+ # # install/update the CouchDB design document
9
+ # Boombera.install_design_doc!('my_database')
10
+ #
11
+ # # connect to the database
12
+ # boombera = Boombera.new('my_database')
13
+ #
14
+ # # put and get some content
15
+ # boombera.put('/hello', 'Hello, world!')
16
+ # #=> true
17
+ #
18
+ # content = boombera.get('/hello')
19
+ # content.body
20
+ # #=> 'Hello, world!'
21
+ #
22
+ # # update the content via the ContentItem
23
+ # content.body = 'Hello, friends!'
24
+ # content.save
25
+ # #=> true
26
+ #
27
+ # # update the content without an object
28
+ # boombera.put('/hello', 'Hello, everyone!')
29
+ # content = boombera.get('/hello')
30
+ # content.body
31
+ # #=> 'Hello, everyone!'
32
+ #
33
+ # # map an alias to the content
34
+ # boombera.map('/hi', '/hello')
35
+ # content = boombera.get('/hi')
36
+ # content.path
37
+ # #=> '/hello'
38
+ #
39
+ # content.body
40
+ # #=> 'Hello, everyone!'
41
+ #
42
+ # # override the map with some different content
43
+ # boombera.put('/hi', "G'day, mate!")
44
+ # content = boombera.get('/hi')
45
+ # content.path
46
+ # #=> '/hi'
47
+ #
48
+ # content.body
49
+ # #=> "G'day, mate!"
50
+ #
51
+ # content = boombera.get('/hello')
52
+ # content.path
53
+ # #=> '/hello'
54
+ #
55
+ # content.body
56
+ # #=> 'Hello, everyone!'
4
57
  class Boombera
5
58
  require 'boombera/content_item'
6
59
  require 'boombera/information'
7
60
 
61
+ # Exception is raised when connecting to a Boombera CouchDB database that
62
+ # expects a different version of the Boombera library than the one currently
63
+ # being used.
8
64
  VersionMismatch = Class.new(StandardError)
65
+
66
+ # Exception is raised when attempting to create a content mapping and the
67
+ # source document doesn't exist.
9
68
  InvalidMapping = Class.new(RuntimeError)
10
69
 
11
70
  extend Boombera::Information
12
71
 
72
+ # The CouchRest::Database instance
13
73
  attr_reader :db
14
74
 
75
+ # Connects to the CouchDB server and verifies the database version.
76
+ #
77
+ # +database_name+:: can be either a full url to a CouchDB server and database
78
+ # (\http://example.com:5984/my_database) or it can be just the database name
79
+ # itself (my_database). The latter will connect to the database at
80
+ # \http://127.0.0.1:5984.
81
+ #
82
+ # raises:: VersionMismatch
15
83
  def initialize(database_name)
16
84
  @db = CouchRest.database!(database_name)
17
85
  check_database_version!
18
86
  end
19
87
 
88
+ # Installs the CouchDB design document for this version of the library in the
89
+ # specified database!
90
+ #
91
+ # *WARNING*:: This will overwrite the current design document and prevent
92
+ # applications that are using a different version of the Boombera library from
93
+ # accessing the database. This change will be replicated along with everything
94
+ # else in your database.
95
+ #
96
+ # +database_name+:: can be either a full url to a CouchDB server and database
97
+ # (\http://example.com:5984/my_database) or it can be just the database name
98
+ # itself (my_database). The latter will connect to the database at
99
+ # \http://127.0.0.1:5984.
100
+ def self.install_design_doc!(database)
101
+ db = CouchRest.database!(database)
102
+ existing = current_design_doc(db)
103
+ design = design_doc
104
+ design['_rev'] = existing['_rev'] unless existing.nil?
105
+ db.save_doc(design)
106
+ end
107
+
108
+ # Creates or updates the content stored at +path+ with +body+.
109
+ #
110
+ # +body+:: can be # any object that can convert to JSON.
111
+ # +path+:: should be a String with /-separated tokens (i.e. "/foo/bar/baz").
20
112
  def put(path, body)
21
- content_item = get(path, :resolve_map => false) and content_item.body = body
113
+ content_item = get_pointer(path) and content_item.body = body
22
114
  content_item ||= ContentItem.new(path, body, db)
23
115
  content_item.save
24
116
  end
25
117
 
26
- def get(path, options = {})
27
- ContentItem.get(path, db, options)
118
+ # Returns the ContentItem associated with the specified path or +nil+ if none
119
+ # is found. If +path+ is mapped to another ContentItem, the resolved
120
+ # ContentItem will be returned.
121
+ def get(path)
122
+ ContentItem::MapResolver.new(path, db).resolve
28
123
  end
29
124
 
125
+ # Creates a content mapping so that two paths can reference the same content
126
+ # without needing to store that content twice.
127
+ #
128
+ # +path+:: the new alias path for the ContentItem
129
+ # +source_path+:: the path of the ContentItem that should be returned for
130
+ # requests to +path+. This path *must* point to an existing ContentItem.
131
+ #
132
+ # raises:: InvalidMapping
30
133
  def map(path, source_path)
31
- content_map = get(path, :resolve_map => false) || ContentItem.new(path, nil, db)
134
+ content_map = get_pointer(path) || ContentItem.new(path, nil, db)
32
135
  content_map.map_to source_path
33
136
  content_map.save
34
137
  end
35
138
 
36
139
  private
37
140
 
141
+ def get_pointer(path)
142
+ ContentItem::MapResolver.new(path, db, :resolve_map => false).resolve
143
+ end
144
+
38
145
  def check_database_version!
39
146
  database_version = Boombera.database_version(db)
40
147
  unless Boombera.version == database_version
@@ -1,5 +1,8 @@
1
+ # ContentItem is a specialization of CouchRest::Document that adds
2
+ # content-mapping semantics and method-based access to the attributes that
3
+ # Boombera knows about.
1
4
  class Boombera::ContentItem < CouchRest::Document
2
- class MapResolver
5
+ class MapResolver #:nodoc: all
3
6
  def initialize(path, database, options = {})
4
7
  @path = path
5
8
  @database = database
@@ -27,16 +30,15 @@ class Boombera::ContentItem < CouchRest::Document
27
30
  end
28
31
  end
29
32
 
30
- class << self
31
- def get(path, database, options = {})
32
- MapResolver.new(path, database, options).resolve
33
- end
34
- end
35
-
33
+ # The actual content that is being stored
36
34
  attr_accessor :body
37
- attr_reader :path, :maps_to
38
35
 
39
- def initialize(doc_or_path, body = nil, database = nil)
36
+ # The path used to access the ContentItem
37
+ attr_reader :path
38
+
39
+ attr_reader :maps_to #:nodoc:
40
+
41
+ def initialize(doc_or_path, body = nil, database = nil) #:nodoc:
40
42
  case doc_or_path
41
43
  when CouchRest::Document
42
44
  @database = doc_or_path.database
@@ -49,7 +51,7 @@ class Boombera::ContentItem < CouchRest::Document
49
51
  end
50
52
  end
51
53
 
52
- def map_to(source_path)
54
+ def map_to(source_path) #:nodoc:
53
55
  rows = @database.view('boombera/content_map', :key => source_path)['rows']
54
56
  if rows.empty?
55
57
  raise Boombera::InvalidMapping,
@@ -60,29 +62,26 @@ class Boombera::ContentItem < CouchRest::Document
60
62
  end
61
63
  end
62
64
 
65
+ # Returns the paths that are aliased to this ContentItem
63
66
  def referenced_by
64
67
  rows = @database.view('boombera/map_references', :key => path)['rows']
65
68
  rows.map{ |row| row['value'] }.sort
66
69
  end
67
70
 
68
- # :nodoc:
69
- def path
71
+ def path #:nodoc:
70
72
  self[:path]
71
73
  end
72
74
 
73
- # :nodoc:
74
- def body
75
+ def body #:nodoc:
75
76
  self[:body]
76
77
  end
77
78
 
78
- # :nodoc:
79
- def body=(new_body)
79
+ def body=(new_body) #:nodoc:
80
80
  self[:body] = new_body
81
81
  self[:maps_to] = path unless new_body.nil?
82
82
  end
83
83
 
84
- # :nodoc:
85
- def maps_to
84
+ def maps_to #:nodoc:
86
85
  self[:maps_to]
87
86
  end
88
87
  end
@@ -1,4 +1,4 @@
1
- module Boombera::Information
1
+ module Boombera::Information #:nodoc: all
2
2
  def version
3
3
  File.read(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'VERSION')))
4
4
  end
@@ -8,14 +8,6 @@ module Boombera::Information
8
8
  doc && doc['gem_version']
9
9
  end
10
10
 
11
- def install_design_doc!(database)
12
- db = CouchRest.database!(database)
13
- existing = current_design_doc(db)
14
- design = design_doc
15
- design['_rev'] = existing['_rev'] unless existing.nil?
16
- db.save_doc(design)
17
- end
18
-
19
11
  def design_doc
20
12
  {
21
13
  '_id' => '_design/boombera',
@@ -0,0 +1,66 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2
+
3
+ describe Boombera::ContentItem::MapResolver do
4
+ describe '#resolve' do
5
+ context 'with an existing content item' do
6
+ it 'returns a ContentItem instance for the found document' do
7
+ view_result = {'rows' => [{'id' => '123', 'value' => '/foo'}]}
8
+ db = mock(CouchRest::Database)
9
+ db.should_receive(:view) \
10
+ .with('boombera/content_map', :key => '/foo') \
11
+ .and_return(view_result)
12
+ db.should_receive(:get) \
13
+ .with('123') \
14
+ .and_return(CouchRest::Document.new('path' => '/foo', 'body' => 'bar'))
15
+ result = Boombera::ContentItem::MapResolver.new('/foo', db).resolve
16
+ result.path.should == '/foo'
17
+ result.body.should == 'bar'
18
+ end
19
+ end
20
+
21
+ context 'with a non-existant content item' do
22
+ it 'returns nil' do
23
+ view_result = {'rows' => []}
24
+ db = mock(CouchRest::Database)
25
+ db.should_receive(:view) \
26
+ .with('boombera/content_map', :key => '/foo') \
27
+ .and_return(view_result)
28
+ Boombera::ContentItem::MapResolver.new('/foo', db).resolve.should == nil
29
+ end
30
+ end
31
+
32
+ context 'with a path that maps to another content item' do
33
+ it 'returns the mapped content item' do
34
+ map_view_result = {'rows' => [{'id' => '123', 'value' => '/bar'}]}
35
+ content_view_result = {'rows' => [{'id' => '456', 'value' => '/bar'}]}
36
+ db = mock(CouchRest::Database)
37
+ db.should_receive(:view) \
38
+ .with('boombera/content_map', :key => '/foo') \
39
+ .and_return(map_view_result)
40
+ db.should_receive(:view) \
41
+ .with('boombera/content_map', :key => '/bar') \
42
+ .and_return(content_view_result)
43
+ db.should_receive(:get) \
44
+ .with('456') \
45
+ .and_return(CouchRest::Document.new('path' => '/bar', 'body' => 'bar'))
46
+ result = Boombera::ContentItem::MapResolver.new('/foo', db).resolve
47
+ result.path.should == '/bar'
48
+ result.body.should == 'bar'
49
+ end
50
+
51
+ it 'returns the pointer content item when passed the :resolve_map option as false' do
52
+ map_view_result = {'rows' => [{'id' => '123', 'value' => '/bar'}]}
53
+ db = mock(CouchRest::Database)
54
+ db.should_receive(:view) \
55
+ .with('boombera/content_map', :key => '/foo') \
56
+ .and_return(map_view_result)
57
+ db.should_receive(:get) \
58
+ .with('123') \
59
+ .and_return(CouchRest::Document.new('path' => '/foo', 'maps_to' => '/bar'))
60
+ result = Boombera::ContentItem::MapResolver.new('/foo', db, :resolve_map => false).resolve
61
+ result.path.should == '/foo'
62
+ result.maps_to.should == '/bar'
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,69 +1,6 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
2
2
 
3
3
  describe Boombera::ContentItem do
4
- describe '.get' do
5
- context 'with an existing content item' do
6
- it 'returns a ContentItem instance for the found document' do
7
- view_result = {'rows' => [{'id' => '123', 'value' => '/foo'}]}
8
- db = mock(CouchRest::Database)
9
- db.should_receive(:view) \
10
- .with('boombera/content_map', :key => '/foo') \
11
- .and_return(view_result)
12
- db.should_receive(:get) \
13
- .with('123') \
14
- .and_return(CouchRest::Document.new('path' => '/foo', 'body' => 'bar'))
15
- result = Boombera::ContentItem.get('/foo', db)
16
- result.path.should == '/foo'
17
- result.body.should == 'bar'
18
- end
19
- end
20
-
21
- context 'with a non-existant content item' do
22
- it 'returns nil' do
23
- view_result = {'rows' => []}
24
- db = mock(CouchRest::Database)
25
- db.should_receive(:view) \
26
- .with('boombera/content_map', :key => '/foo') \
27
- .and_return(view_result)
28
- Boombera::ContentItem.get('/foo', db).should == nil
29
- end
30
- end
31
-
32
- context 'with a path that maps to another content item' do
33
- it 'returns the mapped content item' do
34
- map_view_result = {'rows' => [{'id' => '123', 'value' => '/bar'}]}
35
- content_view_result = {'rows' => [{'id' => '456', 'value' => '/bar'}]}
36
- db = mock(CouchRest::Database)
37
- db.should_receive(:view) \
38
- .with('boombera/content_map', :key => '/foo') \
39
- .and_return(map_view_result)
40
- db.should_receive(:view) \
41
- .with('boombera/content_map', :key => '/bar') \
42
- .and_return(content_view_result)
43
- db.should_receive(:get) \
44
- .with('456') \
45
- .and_return(CouchRest::Document.new('path' => '/bar', 'body' => 'bar'))
46
- result = Boombera::ContentItem.get('/foo', db)
47
- result.path.should == '/bar'
48
- result.body.should == 'bar'
49
- end
50
-
51
- it 'returns the pointer content item when passed the :resolve_map option as false' do
52
- map_view_result = {'rows' => [{'id' => '123', 'value' => '/bar'}]}
53
- db = mock(CouchRest::Database)
54
- db.should_receive(:view) \
55
- .with('boombera/content_map', :key => '/foo') \
56
- .and_return(map_view_result)
57
- db.should_receive(:get) \
58
- .with('123') \
59
- .and_return(CouchRest::Document.new('path' => '/foo', 'maps_to' => '/bar'))
60
- result = Boombera::ContentItem.get('/foo', db, :resolve_map => false)
61
- result.path.should == '/foo'
62
- result.maps_to.should == '/bar'
63
- end
64
- end
65
- end
66
-
67
4
  describe '.new' do
68
5
  context 'when passed a path, body and database' do
69
6
  it 'sets the database from the database argument' do
@@ -40,9 +40,10 @@ describe Boombera do
40
40
  describe '#put' do
41
41
  context "to an existing path" do
42
42
  it 'updates and saves the existing content item' do
43
- Boombera::ContentItem.should_receive(:get) \
43
+ resolver = stub(Boombera::ContentItem::MapResolver, :resolve => content_item)
44
+ Boombera::ContentItem::MapResolver.should_receive(:new) \
44
45
  .with('/foo', db, :resolve_map => false) \
45
- .and_return(content_item)
46
+ .and_return(resolver)
46
47
  content_item.should_receive(:body=).with('bar')
47
48
  content_item.should_receive(:save).and_return(true)
48
49
  boombera.put('/foo', 'bar').should == true
@@ -51,7 +52,7 @@ describe Boombera do
51
52
 
52
53
  context "to a new path" do
53
54
  it 'creates and saves the content item' do
54
- Boombera::ContentItem.stub!(:get => nil)
55
+ Boombera::ContentItem::MapResolver.stub!(:new => stub('resolver', :resolve => nil))
55
56
  Boombera::ContentItem.should_receive(:new) \
56
57
  .with('/foo', 'bar', db) \
57
58
  .and_return(content_item)
@@ -63,16 +64,19 @@ describe Boombera do
63
64
 
64
65
  describe '#get' do
65
66
  it 'gets the content item at the specified path from the current database' do
66
- db.as_null_object
67
- Boombera::ContentItem.should_receive(:get).with('/foo', db, {})
68
- boombera.get('/foo')
67
+ resolver = stub(Boombera::ContentItem::MapResolver, :resolve => :a_content_item)
68
+ Boombera::ContentItem::MapResolver.should_receive(:new) \
69
+ .with('/foo', db) \
70
+ .and_return(resolver)
71
+ boombera.get('/foo').should == :a_content_item
69
72
  end
70
73
  end
71
74
 
72
75
  describe '#map' do
73
76
  context 'to a new path' do
74
77
  before(:each) do
75
- Boombera::ContentItem.stub!(:get)
78
+ resolver = stub(Boombera::ContentItem::MapResolver, :resolve => nil)
79
+ Boombera::ContentItem::MapResolver.stub!(:new => resolver)
76
80
  Boombera::ContentItem.should_receive(:new).with('/bar', nil, db).and_return(content_item)
77
81
  end
78
82
 
@@ -91,9 +95,10 @@ describe Boombera do
91
95
 
92
96
  context 'to an existing path' do
93
97
  before(:each) do
94
- Boombera::ContentItem.should_receive(:get) \
98
+ resolver = stub(Boombera::ContentItem::MapResolver, :resolve => content_item)
99
+ Boombera::ContentItem::MapResolver.should_receive(:new) \
95
100
  .with('/bar', db, :resolve_map => false) \
96
- .and_return(content_item)
101
+ .and_return(resolver)
97
102
  end
98
103
 
99
104
  it 'updates ContentItem as pointer' do
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: boombera
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.1
5
+ version: 0.2.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - John Wilger
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-05-30 00:00:00 -07:00
13
+ date: 2011-05-31 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -148,6 +148,7 @@ files:
148
148
  - lib/boombera/content_item.rb
149
149
  - lib/boombera/information.rb
150
150
  - spec/integration/boombera_spec.rb
151
+ - spec/lib/boombera/content_item_map_resolver_spec.rb
151
152
  - spec/lib/boombera/content_item_spec.rb
152
153
  - spec/lib/boombera_spec.rb
153
154
  - spec/spec_helper.rb
@@ -165,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
165
166
  requirements:
166
167
  - - ">="
167
168
  - !ruby/object:Gem::Version
168
- hash: -422599235011708873
169
+ hash: -2154182263387291460
169
170
  segments:
170
171
  - 0
171
172
  version: "0"