asset_cloud 1.0.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +13 -0
  6. data/Rakefile +24 -0
  7. data/asset_cloud.gemspec +24 -0
  8. data/lib/asset_cloud.rb +54 -0
  9. data/lib/asset_cloud/asset.rb +187 -0
  10. data/lib/asset_cloud/asset_extension.rb +42 -0
  11. data/lib/asset_cloud/base.rb +247 -0
  12. data/lib/asset_cloud/bucket.rb +39 -0
  13. data/lib/asset_cloud/buckets/active_record_bucket.rb +57 -0
  14. data/lib/asset_cloud/buckets/blackhole_bucket.rb +23 -0
  15. data/lib/asset_cloud/buckets/bucket_chain.rb +84 -0
  16. data/lib/asset_cloud/buckets/file_system_bucket.rb +79 -0
  17. data/lib/asset_cloud/buckets/invalid_bucket.rb +28 -0
  18. data/lib/asset_cloud/buckets/memory_bucket.rb +42 -0
  19. data/lib/asset_cloud/buckets/versioned_memory_bucket.rb +33 -0
  20. data/lib/asset_cloud/callbacks.rb +63 -0
  21. data/lib/asset_cloud/free_key_locator.rb +28 -0
  22. data/lib/asset_cloud/metadata.rb +29 -0
  23. data/lib/asset_cloud/validations.rb +52 -0
  24. data/spec/active_record_bucket_spec.rb +95 -0
  25. data/spec/asset_extension_spec.rb +103 -0
  26. data/spec/asset_spec.rb +177 -0
  27. data/spec/base_spec.rb +114 -0
  28. data/spec/blackhole_bucket_spec.rb +41 -0
  29. data/spec/bucket_chain_spec.rb +158 -0
  30. data/spec/callbacks_spec.rb +125 -0
  31. data/spec/file_system_spec.rb +74 -0
  32. data/spec/files/products/key.txt +1 -0
  33. data/spec/files/versioned_stuff/foo +1 -0
  34. data/spec/find_free_key_spec.rb +39 -0
  35. data/spec/memory_bucket_spec.rb +52 -0
  36. data/spec/spec_helper.rb +5 -0
  37. data/spec/validations_spec.rb +53 -0
  38. data/spec/versioned_memory_bucket_spec.rb +36 -0
  39. metadata +151 -0
data/spec/base_spec.rb ADDED
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ class SpecialAsset < AssetCloud::Asset
4
+ end
5
+
6
+ class BasicCloud < AssetCloud::Base
7
+ bucket :special, AssetCloud::MemoryBucket, :asset_class => SpecialAsset
8
+ end
9
+
10
+
11
+ describe BasicCloud do
12
+ directory = File.dirname(__FILE__) + '/files'
13
+
14
+ before do
15
+ @fs = BasicCloud.new(directory , 'http://assets/files' )
16
+ end
17
+
18
+ it "should raise invalid bucket if none is given" do
19
+ @fs['image.jpg'].exist?.should == false
20
+ end
21
+
22
+
23
+ it "should be backed by a file system bucket" do
24
+ @fs['products/key.txt'].exist?.should == true
25
+ end
26
+
27
+ it "should raise when listing non existing buckets" do
28
+ @fs.ls('products').should == [AssetCloud::Asset.new(@fs, 'products/key.txt')]
29
+ end
30
+
31
+
32
+ it "should allow you to create new assets" do
33
+ obj = @fs.build('new_file.test')
34
+ obj.should be_an_instance_of(AssetCloud::Asset)
35
+ obj.cloud.should be_an_instance_of(BasicCloud)
36
+ end
37
+
38
+ it "should raise error when using with minus relative or absolute paths" do
39
+ lambda { @fs['../test'] }.should raise_error(AssetCloud::IllegalPath)
40
+ lambda { @fs['/test'] }.should raise_error(AssetCloud::IllegalPath)
41
+ lambda { @fs['.../test'] }.should raise_error(AssetCloud::IllegalPath)
42
+ lambda { @fs['./test'] }.should raise_error(AssetCloud::IllegalPath)
43
+ end
44
+
45
+ it "should allow sensible relative filenames" do
46
+ @fs['assets/rails_logo.gif']
47
+ @fs['assets/rails-2.gif']
48
+ @fs['assets/223434.gif']
49
+ @fs['files/1.JPG']
50
+ end
51
+
52
+ it "should compute complete urls to assets" do
53
+ @fs.url_for('products/key with spaces.txt').should == 'http://assets/files/products/key%20with%20spaces.txt'
54
+ end
55
+
56
+ describe "#find" do
57
+ it "should return the appropriate asset when one exists" do
58
+ asset = @fs.find('products/key.txt')
59
+ asset.key.should == 'products/key.txt'
60
+ asset.value.should == 'value'
61
+ end
62
+ it "should raise AssetNotFoundError when the asset doesn't exist" do
63
+ lambda { @fs.find('products/not-there.txt') }.should raise_error(AssetCloud::AssetNotFoundError)
64
+ end
65
+ end
66
+
67
+ describe "#[]" do
68
+ it "should return the appropriate asset when one exists" do
69
+ asset = @fs['products/key.txt']
70
+ asset.key.should == 'products/key.txt'
71
+ asset.value.should == 'value'
72
+ end
73
+ it "should not raise any errors when the asset doesn't exist" do
74
+ lambda { @fs['products/not-there.txt'] }.should_not raise_error
75
+ end
76
+ end
77
+
78
+ describe "#[]=" do
79
+ it "should write through the Asset object (and thus run any callbacks on the asset)" do
80
+ special_asset = double(:special_asset)
81
+ special_asset.should_receive(:value=).with('fancy fancy!')
82
+ special_asset.should_receive(:store)
83
+ SpecialAsset.should_receive(:at).and_return(special_asset)
84
+ @fs['special/fancy.txt'] = 'fancy fancy!'
85
+ end
86
+ end
87
+
88
+ describe "#bucket" do
89
+ it "should allow specifying a class to use for assets in this bucket" do
90
+ @fs['assets/rails_logo.gif'].should be_instance_of(AssetCloud::Asset)
91
+ @fs['special/fancy.txt'].should be_instance_of(SpecialAsset)
92
+
93
+ @fs.build('assets/foo').should be_instance_of(AssetCloud::Asset)
94
+ @fs.build('special/foo').should be_instance_of(SpecialAsset)
95
+ end
96
+ end
97
+
98
+ describe "MATCH_BUCKET" do
99
+ it "should match following stuff " do
100
+
101
+ 'products/key.txt' =~ AssetCloud::Base::MATCH_BUCKET
102
+ $1.should == 'products'
103
+
104
+ 'products/subpath/key.txt' =~ AssetCloud::Base::MATCH_BUCKET
105
+ $1.should == 'products'
106
+
107
+ 'key.txt' =~ AssetCloud::Base::MATCH_BUCKET
108
+ $1.should == nil
109
+
110
+ 'products' =~ AssetCloud::Base::MATCH_BUCKET
111
+ $1.should == 'products'
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ class BlackholeCloud < AssetCloud::Base
4
+ bucket AssetCloud::BlackholeBucket
5
+ end
6
+
7
+ describe BlackholeCloud do
8
+ directory = File.dirname(__FILE__) + '/files'
9
+
10
+ before do
11
+ @fs = BlackholeCloud.new(directory , 'http://assets/files' )
12
+ end
13
+
14
+ it "should allow access to files using the [] operator" do
15
+ @fs['tmp/image.jpg']
16
+ end
17
+
18
+ it "should return nil for non existent files" do
19
+ @fs['tmp/image.jpg'].exist?.should == false
20
+ end
21
+
22
+ it "should still return nil, even if you wrote something there" do
23
+ @fs['tmp/image.jpg'] = 'test'
24
+ @fs['tmp/image.jpg'].exist?.should == false
25
+ end
26
+
27
+ describe "when using a sub path" do
28
+ it "should allow access to files using the [] operator" do
29
+ @fs['tmp/image.jpg']
30
+ end
31
+
32
+ it "should return nil for non existent files" do
33
+ @fs['tmp/image.jpg'].exist?.should == false
34
+ end
35
+
36
+ it "should still return nil, even if you wrote something there" do
37
+ @fs['tmp/image.jpg'] = 'test'
38
+ @fs['tmp/image.jpg'].exist?.should == false
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ class ChainedCloud < AssetCloud::Base
4
+ bucket :stuff, AssetCloud::BucketChain.chain( AssetCloud::MemoryBucket,
5
+ AssetCloud::MemoryBucket,
6
+ AssetCloud::FileSystemBucket )
7
+
8
+ bucket :versioned_stuff, AssetCloud::BucketChain.chain( AssetCloud::FileSystemBucket,
9
+ AssetCloud::VersionedMemoryBucket,
10
+ AssetCloud::MemoryBucket )
11
+ end
12
+
13
+ describe AssetCloud::BucketChain do
14
+ directory = File.dirname(__FILE__) + '/files'
15
+
16
+ before(:each) do
17
+ @cloud = ChainedCloud.new(directory , 'http://assets/files' )
18
+ @bucket_chain = @cloud.buckets[:stuff]
19
+ @chained_buckets = @bucket_chain.chained_buckets
20
+ @chained_buckets.each {|b| b.ls('stuff').each {|asset| asset.delete}}
21
+
22
+ @versioned_stuff = @cloud.buckets[:versioned_stuff]
23
+ end
24
+
25
+ describe ".chain" do
26
+ it 'should take multiple Bucket classes and return a new Bucket class' do
27
+ @bucket_chain.should be_a_kind_of(AssetCloud::BucketChain)
28
+ end
29
+ end
30
+
31
+ describe "#write" do
32
+ it 'should write to each sub-bucket when everything is kosher and return the result of the first write' do
33
+ @chained_buckets.each do |bucket|
34
+ bucket.should_receive(:write).with('stuff/foo', 'successful creation').and_return('successful creation')
35
+ end
36
+
37
+ @bucket_chain.write('stuff/foo', 'successful creation').should == 'successful creation'
38
+ end
39
+ it 'should roll back creation-writes and re-raise an error when a bucket raises one' do
40
+ @chained_buckets.last.should_receive(:write).with('stuff/foo', 'unsuccessful creation').and_raise('hell')
41
+ @chained_buckets[0..-2].each do |bucket|
42
+ bucket.should_receive(:write).with('stuff/foo', 'unsuccessful creation').and_return(true)
43
+ bucket.should_receive(:delete).with('stuff/foo').and_return(true)
44
+ end
45
+
46
+ lambda { @bucket_chain.write('stuff/foo', 'unsuccessful creation') }.should raise_error(RuntimeError)
47
+ end
48
+ it 'should roll back update-writes and re-raise an error when a bucket raises one' do
49
+ @bucket_chain.write('stuff/foo', "original value")
50
+
51
+ @chained_buckets.last.should_receive(:write).with('stuff/foo', 'new value').and_raise('hell')
52
+
53
+ lambda { @bucket_chain.write('stuff/foo', 'new value') }.should raise_error(RuntimeError)
54
+ @chained_buckets.each do |bucket|
55
+ bucket.read('stuff/foo').should == 'original value'
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "#delete" do
61
+ it 'should delete from each sub-bucket when everything is kosher' do
62
+ @bucket_chain.write('stuff/foo', "successful deletion comin' up")
63
+
64
+ @chained_buckets.each do |bucket|
65
+ bucket.should_receive(:delete).with('stuff/foo').and_return(true)
66
+ end
67
+
68
+ @bucket_chain.delete('stuff/foo')
69
+ end
70
+ it 'should roll back deletions and re-raise an error when a bucket raises one' do
71
+ @bucket_chain.write('stuff/foo', "this deletion will fail")
72
+
73
+ @chained_buckets.last.should_receive(:delete).with('stuff/foo').and_raise('hell')
74
+ @chained_buckets[0..-2].each do |bucket|
75
+ bucket.should_receive(:delete).with('stuff/foo').and_return(true)
76
+ bucket.should_receive(:write).with('stuff/foo', 'this deletion will fail').and_return(true)
77
+ end
78
+
79
+ lambda { @bucket_chain.delete('stuff/foo') }.should raise_error(RuntimeError)
80
+ end
81
+ end
82
+
83
+ describe "#read" do
84
+ it 'should read from only the first available sub-bucket' do
85
+ @chained_buckets[0].should_receive(:read).with('stuff/foo').and_raise(NotImplementedError)
86
+ @chained_buckets[0].should_receive(:ls).with(nil).and_raise(NoMethodError)
87
+ @chained_buckets[0].should_receive(:stat).and_return(:metadata)
88
+
89
+ @chained_buckets[1].should_receive(:read).with('stuff/foo').and_return('bar')
90
+ @chained_buckets[1].should_receive(:ls).with(nil).and_return(:some_assets)
91
+ @chained_buckets[1].should_not_receive(:stat)
92
+
93
+ @chained_buckets[2..-1].each do |bucket|
94
+ bucket.should_not_receive(:read)
95
+ bucket.should_not_receive(:ls)
96
+ bucket.should_not_receive(:stat)
97
+ end
98
+
99
+ @bucket_chain.read('stuff/foo').should == 'bar'
100
+ @bucket_chain.ls.should == :some_assets
101
+ @bucket_chain.stat.should == :metadata
102
+ end
103
+ end
104
+
105
+
106
+ describe "#read_version" do
107
+ it 'should read from only the first available sub-bucket' do
108
+ buckets = @versioned_stuff.chained_buckets
109
+
110
+ buckets[1].should_receive(:read_version).with('stuff/foo',3).and_return('bar')
111
+ buckets.last.should_not_receive(:read_version)
112
+
113
+ @versioned_stuff.read_version('stuff/foo', 3).should == 'bar'
114
+ end
115
+ end
116
+
117
+ describe "#versions" do
118
+ it 'should read from only the first available sub-bucket' do
119
+ buckets = @versioned_stuff.chained_buckets
120
+
121
+ buckets[1].should_receive(:versions).with('versioned_stuff/foo').and_return([1,2,3])
122
+ buckets.last.should_not_receive(:versions)
123
+
124
+ @versioned_stuff.versions('versioned_stuff/foo').should == [1,2,3]
125
+ end
126
+ end
127
+
128
+ describe "with versioned buckets" do
129
+ it 'should store and retrieve versions seamlessly' do
130
+ %w{one two three}.each do |content|
131
+ @cloud['versioned_stuff/foo'] = content
132
+ end
133
+ asset = @cloud['versioned_stuff/foo']
134
+ asset.value.should == 'three'
135
+ asset.rollback(1).value.should == 'one'
136
+ asset.versions.should == [1,2,3]
137
+ asset.value = 'four'
138
+ asset.store
139
+ asset.versions.should == [1,2,3,4]
140
+ end
141
+ end
142
+
143
+ describe '#respond_to?' do
144
+ it 'should return true if any chained buckets respond to the given method' do
145
+ @bucket_chain.respond_to?(:foo).should == false
146
+ @chained_buckets[1].should_receive(:respond_to?).with(:bar).and_return(true)
147
+ @bucket_chain.respond_to?(:bar).should == true
148
+ end
149
+ end
150
+
151
+ describe '#method_missing' do
152
+ it 'should try each bucket' do
153
+ @chained_buckets[1].should_receive(:buzz).and_return(true)
154
+ @chained_buckets[2].should_not_receive(:buzz)
155
+ @bucket_chain.buzz.should == true
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ class CallbackAsset < AssetCloud::Asset
4
+ before_store :callback_before_store
5
+ after_delete :callback_after_delete
6
+ before_validate :make_value_valid
7
+ after_validate :add_spice
8
+ validate :valid_value
9
+
10
+ private
11
+ def make_value_valid
12
+ self.value = 'valid'
13
+ end
14
+ def add_spice
15
+ self.value += ' spice'
16
+ end
17
+
18
+ def valid_value
19
+ add_error 'value is not "valid"' unless value == 'valid'
20
+ end
21
+ end
22
+
23
+ class BasicCloud < AssetCloud::Base
24
+ bucket :callback_assets, AssetCloud::MemoryBucket, :asset_class => CallbackAsset
25
+ end
26
+
27
+ class CallbackCloud < AssetCloud::Base
28
+ bucket :tmp, AssetCloud::MemoryBucket
29
+
30
+ after_delete :callback_after_delete
31
+ before_delete :callback_before_delete
32
+
33
+ after_write :callback_after_write
34
+ before_write :callback_before_write
35
+ end
36
+
37
+ class MethodRecordingCloud < AssetCloud::Base
38
+ attr_accessor :run_callbacks
39
+
40
+ bucket :tmp, AssetCloud::MemoryBucket
41
+
42
+ before_write :callback_before_write
43
+ after_write :callback_before_write
44
+
45
+
46
+ def method_missing(method, *args)
47
+ @run_callbacks << method.to_sym
48
+ end
49
+ end
50
+
51
+ describe CallbackCloud do
52
+ before { @fs = CallbackCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/') }
53
+
54
+ it "should invoke callbacks after store" do
55
+
56
+ @fs.should_receive(:callback_before_write).with('tmp/file.txt', 'text').and_return(true)
57
+ @fs.should_receive(:callback_after_write).with('tmp/file.txt', 'text').and_return(true)
58
+
59
+
60
+ @fs.write 'tmp/file.txt', 'text'
61
+
62
+ end
63
+
64
+ it "should invoke callbacks after delete" do
65
+
66
+ @fs.should_receive(:callback_before_delete).with('tmp/file.txt').and_return(true)
67
+ @fs.should_receive(:callback_after_delete).with('tmp/file.txt').and_return(true)
68
+
69
+
70
+ @fs.delete 'tmp/file.txt'
71
+ end
72
+
73
+ it "should invoke callbacks even when constructing a new asset" do
74
+ @fs.should_receive(:callback_before_write).with('tmp/file.txt', 'hello').and_return(true)
75
+ @fs.should_receive(:callback_after_write).with('tmp/file.txt', 'hello').and_return(true)
76
+
77
+
78
+ asset = @fs.build('tmp/file.txt')
79
+ asset.value = 'hello'
80
+ asset.store
81
+
82
+ end
83
+
84
+ end
85
+
86
+ describe MethodRecordingCloud do
87
+ before do
88
+ @fs = MethodRecordingCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
89
+ @fs.run_callbacks = []
90
+ end
91
+
92
+ it 'should record event when invoked' do
93
+ @fs.write('tmp/file.txt', 'random data')
94
+ @fs.run_callbacks.should == [:callback_before_write, :callback_before_write]
95
+ end
96
+
97
+ it 'should record event when assignment operator is used' do
98
+ @fs['tmp/file.txt'] = 'random data'
99
+ @fs.run_callbacks.should == [:callback_before_write, :callback_before_write]
100
+ end
101
+ end
102
+
103
+ describe CallbackAsset do
104
+ before(:each) do
105
+ @fs = BasicCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
106
+ @fs.write('callback_assets/foo', 'bar')
107
+ @asset = @fs.asset_at('callback_assets/foo')
108
+ end
109
+
110
+ it "should run before_validate, then validate, then after validate, then before_store, then store" do
111
+ @asset.should_receive(:callback_before_store).and_return(true)
112
+ @asset.should_not_receive(:callback_after_delete)
113
+
114
+ @asset.value = 'foo'
115
+ @asset.store.should == true
116
+ @asset.value.should == 'valid spice'
117
+ end
118
+
119
+ it "should run its after_delete callback after delete is called" do
120
+ @asset.should_not_receive(:callback_before_store)
121
+ @asset.should_receive(:callback_after_delete).and_return(true)
122
+
123
+ @asset.delete
124
+ end
125
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ class FileSystemCloud < AssetCloud::Base
5
+ bucket AssetCloud::InvalidBucket
6
+ bucket :products, AssetCloud::FileSystemBucket
7
+ bucket :tmp, AssetCloud::FileSystemBucket
8
+ end
9
+
10
+
11
+ describe FileSystemCloud do
12
+ directory = File.dirname(__FILE__) + '/files'
13
+
14
+ before do
15
+ @fs = FileSystemCloud.new(directory , 'http://assets/files' )
16
+ FileUtils.mkdir_p(directory + '/tmp')
17
+ end
18
+
19
+ after do
20
+ FileUtils.rm_rf(directory + '/tmp')
21
+ end
22
+
23
+ it "should use invalid bucket for random directories" do
24
+ @fs.bucket_for('does-not-exist/file.txt').should be_an_instance_of(AssetCloud::InvalidBucket)
25
+ end
26
+
27
+ it "should use filesystem bucekt for products/ and tmp/ directories" do
28
+ @fs.bucket_for('products/file.txt').should be_an_instance_of(AssetCloud::FileSystemBucket)
29
+ @fs.bucket_for('tmp/file.txt').should be_an_instance_of(AssetCloud::FileSystemBucket)
30
+ end
31
+
32
+ it "should return Asset for existing files" do
33
+ @fs['products/key.txt'].exist?.should == true
34
+ @fs['products/key.txt'].should be_an_instance_of(AssetCloud::Asset)
35
+ end
36
+
37
+ it "should be able to test if a file exists or not" do
38
+ @fs.stat('products/key.txt').exist?.should == true
39
+ @fs.stat('products/key2.txt').exist?.should == false
40
+ end
41
+
42
+ it "should be able to list files" do
43
+ @fs.ls('products').collect(&:key).should == ['products/key.txt']
44
+ end
45
+
46
+ describe 'when modifying file system' do
47
+
48
+ it "should call write after storing an asset" do
49
+ @fs.buckets[:tmp].should_receive(:write).with('tmp/new_file.test', 'hello world').and_return(true)
50
+
51
+ @fs.build('tmp/new_file.test', 'hello world').store
52
+ end
53
+
54
+ it "should be able to create new files" do
55
+ @fs.build('tmp/new_file.test', 'hello world').store
56
+
57
+ @fs.stat('tmp/new_file.test').exist.should == true
58
+ end
59
+
60
+ it "should be able to create new files with simple assignment" do
61
+ @fs['tmp/new_file.test'] = 'hello world'
62
+
63
+ @fs.stat('tmp/new_file.test').exist.should == true
64
+ end
65
+
66
+ it "should create directories as needed" do
67
+ @fs.build('tmp/new_file.test', 'hello world').store
68
+
69
+ @fs['tmp/new_file.test'].exist?.should == true
70
+ @fs['tmp/new_file.test'].value.should == 'hello world'
71
+ end
72
+
73
+ end
74
+ end