asset_cloud 1.0.2

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