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
@@ -0,0 +1,28 @@
1
+ require 'securerandom'
2
+
3
+ module AssetCloud
4
+
5
+ module FreeKeyLocator
6
+
7
+ def find_free_key_like(key, options = {})
8
+ # Check weather the suggested key name is free. If so we
9
+ # simply return it.
10
+
11
+ if not exist?(key)
12
+ key
13
+ else
14
+
15
+ ext = File.extname(key)
16
+ dirname = File.dirname(key)
17
+ base = dirname == '.' ? File.basename(key, ext) : File.join(File.dirname(key), File.basename(key, ext))
18
+ base = base.gsub(/_[\h]{8}-[\h]{4}-4[\h]{3}-[\h]{4}-[\h]{12}/, "")
19
+
20
+ # Attach UUID to avoid name collision
21
+ key = "#{base}_#{SecureRandom.uuid}#{ext}"
22
+ return key unless exist?(key)
23
+
24
+ raise StandardError, 'Filesystem out of free filenames'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module AssetCloud
2
+ class Metadata
3
+ attr_accessor :exist, :size, :created_at, :updated_at, :value_hash
4
+
5
+ def new?
6
+ !self.exist
7
+ end
8
+
9
+ def exist?
10
+ self.exist
11
+ end
12
+
13
+ def initialize(exist, size = nil, created_at = nil, updated_at = nil, value_hash = nil)
14
+ self.exist, self.size, self.created_at, self.updated_at, self.value_hash = exist, size, created_at, updated_at, value_hash
15
+ end
16
+
17
+ def self.existing
18
+ self.new(true)
19
+ end
20
+
21
+ def self.non_existing
22
+ self.new false
23
+ end
24
+
25
+ def inspect
26
+ "#<#{self.class.name}: exist:#{exist} size:#{size.inspect} bytes>"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ module AssetCloud
2
+ module Validations
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.class_eval do
6
+ include AssetCloud::Callbacks
7
+
8
+ alias_method_chain :store, :validation
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def validate(*validations, &block)
14
+ validations << block if block_given?
15
+ write_inheritable_array(:validate, validations)
16
+ end
17
+ end
18
+
19
+ def store_with_validation
20
+ validate
21
+ errors.empty? ? store_without_validation : false
22
+ end
23
+
24
+ def errors
25
+ @errors ||= []
26
+ end
27
+
28
+ def warnings
29
+ @warnings ||= []
30
+ end
31
+
32
+ def valid?
33
+ validate
34
+ errors.empty?
35
+ end
36
+
37
+ def add_error(msg)
38
+ errors << msg
39
+ errors.uniq!
40
+ end
41
+
42
+ def add_warning(*msgs)
43
+ warnings.concat(msgs)
44
+ end
45
+
46
+ def validate
47
+ errors.clear
48
+ warnings.clear
49
+ execute_callbacks(:validate, [])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ MockRecords = Object.new
4
+
5
+ class MockActiveRecordBucket < AssetCloud::ActiveRecordBucket
6
+ self.key_attribute = 'name'
7
+ self.value_attribute = 'body'
8
+ protected
9
+ def records
10
+ MockRecords
11
+ end
12
+ end
13
+
14
+ class RecordCloud < AssetCloud::Base
15
+ bucket :stuff, MockActiveRecordBucket
16
+ end
17
+
18
+ describe AssetCloud::ActiveRecordBucket do
19
+ directory = File.dirname(__FILE__) + '/files'
20
+
21
+ before do
22
+ @cloud = RecordCloud.new(directory , 'http://assets/files' )
23
+ @bucket = @cloud.buckets[:stuff]
24
+ end
25
+
26
+ describe '#ls' do
27
+ before do
28
+ MockRecords.should_receive(:connection).and_return(@mock_connection = double("connection"))
29
+ @mock_connection.should_receive(:quote_column_name).with('name').and_return("`name`")
30
+ (@mock_record = double("record")).should_receive(:name).and_return('stuff/a1')
31
+ end
32
+
33
+ it "should return a list of assets which start with the given prefix" do
34
+ MockRecords.should_receive(:all).with(:conditions => ["`name` LIKE ?", "stuff/a%"]).and_return([@mock_record])
35
+
36
+ @bucket.ls('stuff/a').size.should == 1
37
+ end
38
+
39
+ it "should return a list of all assets when a prefix is not given" do
40
+ MockRecords.should_receive(:all).with(:conditions => ["`name` LIKE ?", "stuff%"]).and_return([@mock_record])
41
+
42
+ @bucket.ls.size.should == 1
43
+ end
44
+ end
45
+
46
+ describe '#read' do
47
+ it "should return the value of a key when it exists" do
48
+ (@mock_record = double("record")).should_receive(:body).and_return('foo')
49
+ MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(@mock_record)
50
+
51
+ @bucket.read('stuff/a1')
52
+ end
53
+ it "should raise AssetNotFoundError when nothing is there" do
54
+ MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(nil)
55
+
56
+ lambda {@bucket.read('stuff/a1')}.should raise_error(AssetCloud::AssetNotFoundError)
57
+ end
58
+ end
59
+
60
+ describe '#write' do
61
+ it "should write to the DB" do
62
+ (@mock_record = double("record")).should_receive(:body=).with('foo').and_return('foo')
63
+ @mock_record.should_receive(:save!).and_return(true)
64
+ MockRecords.should_receive(:find_or_initialize_by_name).with('stuff/a1').and_return(@mock_record)
65
+
66
+ @bucket.write('stuff/a1', 'foo')
67
+ end
68
+ end
69
+
70
+ describe '#delete' do
71
+ it "should destroy records" do
72
+ (@mock_record = double("record")).should_receive(:destroy).and_return(true)
73
+ MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(@mock_record)
74
+
75
+ @bucket.delete('stuff/a1')
76
+ end
77
+ end
78
+
79
+ describe '#stat' do
80
+ it "should return appropriate metadata" do
81
+ (@mock_record = double("record")).should_receive(:created_at).and_return(1982)
82
+ @mock_record.should_receive(:updated_at).and_return(2002)
83
+ @mock_record.should_receive(:body).and_return('foo')
84
+ MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(@mock_record)
85
+
86
+ metadata = @bucket.stat('stuff/a1')
87
+ metadata.created_at.should == 1982
88
+ metadata.updated_at.should == 2002
89
+ metadata.size.should == 3
90
+ end
91
+ end
92
+
93
+
94
+
95
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ class NoCatsAsset < AssetCloud::Asset
4
+ validate :no_cats
5
+ before_store :asset_callback
6
+
7
+ private
8
+ def no_cats
9
+ add_error('no cats allowed!') if value =~ /cat/i
10
+ end
11
+ end
12
+
13
+ class CssAssetExtension < AssetCloud::AssetExtension
14
+ applies_to :css
15
+
16
+ validate :valid_css
17
+
18
+ private
19
+ def valid_css
20
+ add_error "not enough curly brackets!" unless asset.value =~ /\{.*\}/
21
+ end
22
+ end
23
+
24
+ class XmlAssetExtension < AssetCloud::AssetExtension
25
+ applies_to :xml
26
+
27
+ validate :valid_xml
28
+ before_store :xml_callback
29
+
30
+ def turn_into_xml
31
+ asset.value = "<xml>#{asset.value}</xml>"
32
+ end
33
+
34
+ private
35
+ def valid_xml
36
+ add_error "not enough angle brackets!" unless asset.value =~ /\<.*\>/
37
+ end
38
+ end
39
+
40
+ class CatsAndDogsCloud < AssetCloud::Base
41
+ bucket :dog_pound, AssetCloud::MemoryBucket, :asset_class => NoCatsAsset
42
+ bucket :cat_pen, AssetCloud::MemoryBucket
43
+
44
+ asset_extensions CssAssetExtension, :only => :cat_pen
45
+ asset_extensions XmlAssetExtension, :except => :cat_pen
46
+ end
47
+
48
+ describe "AssetExtension" do
49
+ include AssetCloud
50
+
51
+ before do
52
+ @cloud = CatsAndDogsCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
53
+ end
54
+
55
+ describe "applicability" do
56
+ it "should work" do
57
+ asset = @cloud['cat_pen/cats.xml']
58
+ XmlAssetExtension.applies_to_asset?(asset).should == true
59
+ end
60
+ end
61
+
62
+ describe "validations" do
63
+ it "should be added to assets in the right bucket with the right extension" do
64
+ asset = @cloud['cat_pen/cats.css']
65
+ asset.value = 'foo'
66
+ asset.store.should == false
67
+ asset.errors.should == ["not enough curly brackets!"]
68
+ end
69
+
70
+ it "should not squash existing validations on the asset" do
71
+ asset = @cloud['dog_pound/cats.xml']
72
+ asset.value = 'cats!'
73
+ asset.store.should == false
74
+ asset.errors.should == ['no cats allowed!', "not enough angle brackets!"]
75
+ end
76
+
77
+ it "should not apply to non-matching assets or those in exempted buckets" do
78
+ asset = @cloud['cat_pen/cats.xml']
79
+ asset.value = "xml"
80
+ asset.store.should == true
81
+ end
82
+ end
83
+
84
+ describe "callbacks" do
85
+ it "should run alongside the asset's callbacks" do
86
+ asset = @cloud['dog_pound/dogs.xml']
87
+ asset.should_receive(:asset_callback)
88
+ asset.extensions.first.should_receive(:xml_callback)
89
+ asset.value = '<dogs/>'
90
+ asset.store.should == true
91
+ end
92
+ end
93
+
94
+ describe "#method_missing" do
95
+ it "should try to run method on extensions" do
96
+ asset = @cloud['dog_pound/dogs.xml']
97
+ asset.value = 'dogs'
98
+ asset.turn_into_xml
99
+ asset.value.should == '<xml>dogs</xml>'
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,177 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Asset" do
4
+ include AssetCloud
5
+
6
+ before do
7
+ @cloud = double('Cloud', :asset_extension_classes_for_bucket => [])
8
+ end
9
+
10
+ describe "when first created (without a value)" do
11
+ before do
12
+ @asset = AssetCloud::Asset.new(@cloud, "products/key.txt")
13
+ end
14
+
15
+ it "should be return new_asset? => true" do
16
+ @asset.new_asset?.should == true
17
+ end
18
+
19
+ it "should have a key" do
20
+ @asset.key.should == 'products/key.txt'
21
+ end
22
+
23
+ it "should have a value of nil" do
24
+
25
+ @asset.value.should == nil
26
+ end
27
+
28
+ it "should have a basename" do
29
+ @asset.basename.should == 'key.txt'
30
+ end
31
+
32
+ it "should have a basename without ext (if required)" do
33
+ @asset.basename_without_ext.should == 'key'
34
+ end
35
+
36
+ it "should have an ext" do
37
+ @asset.extname.should == '.txt'
38
+ end
39
+
40
+ it "should have a relative_key_without_ext" do
41
+ @asset.relative_key_without_ext.should == 'key'
42
+ end
43
+
44
+ it "should have a bucket_name" do
45
+ @asset.bucket_name.should == 'products'
46
+ end
47
+
48
+ it "should have a bucket" do
49
+ @cloud.should_receive(:buckets).and_return(:products => :products_bucket)
50
+ @asset.bucket.should == :products_bucket
51
+ end
52
+
53
+ it "should store data to the bucket" do
54
+ @cloud.should_receive(:write).with("products/key.txt", 'value')
55
+
56
+ @asset.value = 'value'
57
+ @asset.store
58
+ end
59
+
60
+ it "should not try to store data when it's value is nil" do
61
+ @cloud.should_receive(:write).never
62
+
63
+ @asset.store
64
+ end
65
+
66
+ it "should not try to read data from bucket if its a new_asset" do
67
+ @cloud.should_receive(:read).never
68
+
69
+ @asset.value.should == nil
70
+ end
71
+
72
+ it "should simply ignore calls to delete" do
73
+ @cloud.should_receive(:delete).never
74
+
75
+ @asset.delete
76
+ end
77
+
78
+ end
79
+
80
+
81
+ describe "when first created (without a value) with subdirectory" do
82
+ before do
83
+ @asset = AssetCloud::Asset.new(@cloud, "products/retail/key.txt")
84
+ end
85
+
86
+ it "should have a relative_key_without_ext" do
87
+ @asset.relative_key_without_ext.should == 'retail/key'
88
+ end
89
+
90
+ it "should have a relative_key" do
91
+ @asset.relative_key.should == 'retail/key.txt'
92
+ end
93
+ end
94
+
95
+
96
+ describe "when first created with value" do
97
+ before do
98
+ @asset = AssetCloud::Asset.new(@cloud, "products/key.txt", 'value')
99
+ end
100
+
101
+ it "should be return new_asset? => true" do
102
+ @asset.new_asset?.should == true
103
+ end
104
+
105
+
106
+ it "should have a value of 'value'" do
107
+ @asset.value.should == 'value'
108
+ end
109
+
110
+ it "should return false when asked if it exists because its still a new_asset" do
111
+ @asset.exist?.should == false
112
+ end
113
+
114
+
115
+ it "should not try to read data from bucket if its a new_asset" do
116
+ @cloud.should_receive(:read).never
117
+
118
+ @asset.value.should == 'value'
119
+ end
120
+
121
+ it "should write data to the bucket" do
122
+ @cloud.should_receive(:write).with("products/key.txt", 'value')
123
+ @asset.store
124
+ end
125
+
126
+ end
127
+
128
+ describe "when fetched from the bucket" do
129
+ before do
130
+ @asset = AssetCloud::Asset.at(@cloud, "products/key.txt", 'value', AssetCloud::Metadata.new(true, 'value'.size, Time.now, Time.now))
131
+ end
132
+
133
+ it "should be return new_asset? => false" do
134
+ @asset.new_asset?.should == false
135
+ end
136
+
137
+ it "should indicate that it exists" do
138
+
139
+ @asset.exist?.should == true
140
+ end
141
+
142
+
143
+ it "should read the value from the bucket" do
144
+ @asset.value.should == 'value'
145
+ end
146
+
147
+
148
+ it "should simply ignore calls to delete" do
149
+ @cloud.should_receive(:delete).and_return(true)
150
+
151
+ @asset.delete
152
+ end
153
+
154
+ it "should ask the bucket to create a full url" do
155
+ @cloud.should_receive(:url_for).with('products/key.txt', {}).and_return('http://assets/products/key.txt')
156
+
157
+ @asset.url.should == 'http://assets/products/key.txt'
158
+ end
159
+
160
+ it "should ask the bucket whether or not it is versioned" do
161
+ bucket = double('Bucket')
162
+ @cloud.should_receive(:buckets).and_return(:products => bucket)
163
+ bucket.should_receive(:versioned?).and_return(true)
164
+
165
+ @asset.versioned?.should == true
166
+ end
167
+
168
+ it "should validate its key" do
169
+ asset = AssetCloud::Asset.new(@cloud, "products/foo, bar.txt", "data")
170
+ asset.store.should == false
171
+ asset.errors.size.should == 1
172
+ asset.errors.first.should =~ /illegal characters/
173
+ end
174
+ end
175
+
176
+
177
+ end