boombera 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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"