jamesmacaulay-asset_cloud 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,7 +1,14 @@
1
+ July 2009:
2
+
3
+ * Added ActiveRecordBucket
4
+
1
5
  June 2009:
2
6
 
7
+ * Buckets can be versioned
8
+ * Transactional writes with BucketChains
9
+ * Asset callbacks
3
10
  * Use different Asset subclasses for each bucket in a cloud (james)
4
11
  * AssetCloud::Base#find now calls asset_at but will raise errors if the asset doesn't exist (james)
5
12
  * original AssetCloud::Base#find renamed to #asset_at (james)
6
13
  * Added simple bucket multiplexing with AssetCloud::Bucket.chain (james)
7
- * extraction from Shopify
14
+ * extraction from Shopify (tobi)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.2
data/asset_cloud.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{asset_cloud}
5
- s.version = "0.5.1"
5
+ s.version = "0.5.2"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Shopify"]
9
- s.date = %q{2009-06-17}
9
+ s.date = %q{2009-07-03}
10
10
  s.description = %q{An abstraction layer around arbitrary and diverse asset stores.}
11
11
  s.email = %q{developers@shopify.com}
12
12
  s.extra_rdoc_files = [
@@ -27,44 +27,54 @@ Gem::Specification.new do |s|
27
27
  "lib/asset_cloud.rb",
28
28
  "lib/asset_cloud/asset.rb",
29
29
  "lib/asset_cloud/base.rb",
30
- "lib/asset_cloud/blackhole_bucket.rb",
31
30
  "lib/asset_cloud/bucket.rb",
31
+ "lib/asset_cloud/buckets/active_record_bucket.rb",
32
+ "lib/asset_cloud/buckets/blackhole_bucket.rb",
33
+ "lib/asset_cloud/buckets/bucket_chain.rb",
34
+ "lib/asset_cloud/buckets/file_system_bucket.rb",
35
+ "lib/asset_cloud/buckets/invalid_bucket.rb",
36
+ "lib/asset_cloud/buckets/memory_bucket.rb",
37
+ "lib/asset_cloud/buckets/versioned_memory_bucket.rb",
32
38
  "lib/asset_cloud/callbacks.rb",
33
- "lib/asset_cloud/file_system_bucket.rb",
34
39
  "lib/asset_cloud/free_key_locator.rb",
35
- "lib/asset_cloud/invalid_bucket.rb",
36
- "lib/asset_cloud/memory_bucket.rb",
37
40
  "lib/asset_cloud/metadata.rb",
41
+ "lib/asset_cloud/validations.rb",
42
+ "spec/active_record_bucket_spec.rb",
38
43
  "spec/asset_spec.rb",
39
44
  "spec/base_spec.rb",
40
45
  "spec/blackhole_bucket_spec.rb",
41
- "spec/bucket_spec.rb",
46
+ "spec/bucket_chain_spec.rb",
42
47
  "spec/callbacks_spec.rb",
43
48
  "spec/file_system_spec.rb",
44
49
  "spec/files/products/key.txt",
50
+ "spec/files/versioned_stuff/foo",
45
51
  "spec/find_free_key_spec.rb",
46
52
  "spec/memory_bucket_spec.rb",
47
53
  "spec/regexp_spec.rb",
48
54
  "spec/spec.opts",
49
- "spec/spec_helper.rb"
55
+ "spec/spec_helper.rb",
56
+ "spec/validations_spec.rb",
57
+ "spec/versioned_memory_bucket_spec.rb"
50
58
  ]
51
- s.has_rdoc = true
52
59
  s.homepage = %q{http://github.com/Shopify/asset_cloud}
53
60
  s.rdoc_options = ["--charset=UTF-8"]
54
61
  s.require_paths = ["lib"]
55
- s.rubygems_version = %q{1.3.2}
62
+ s.rubygems_version = %q{1.3.4}
56
63
  s.summary = %q{An abstraction layer around arbitrary and diverse asset stores.}
57
64
  s.test_files = [
58
- "spec/asset_spec.rb",
65
+ "spec/active_record_bucket_spec.rb",
66
+ "spec/asset_spec.rb",
59
67
  "spec/base_spec.rb",
60
68
  "spec/blackhole_bucket_spec.rb",
61
- "spec/bucket_spec.rb",
69
+ "spec/bucket_chain_spec.rb",
62
70
  "spec/callbacks_spec.rb",
63
71
  "spec/file_system_spec.rb",
64
72
  "spec/find_free_key_spec.rb",
65
73
  "spec/memory_bucket_spec.rb",
66
74
  "spec/regexp_spec.rb",
67
- "spec/spec_helper.rb"
75
+ "spec/spec_helper.rb",
76
+ "spec/validations_spec.rb",
77
+ "spec/versioned_memory_bucket_spec.rb"
68
78
  ]
69
79
 
70
80
  if s.respond_to? :specification_version then
data/lib/asset_cloud.rb CHANGED
@@ -4,20 +4,32 @@ require 'active_support'
4
4
  require File.dirname(__FILE__) + '/asset_cloud/asset'
5
5
  require File.dirname(__FILE__) + '/asset_cloud/metadata'
6
6
  require File.dirname(__FILE__) + '/asset_cloud/bucket'
7
- require File.dirname(__FILE__) + '/asset_cloud/invalid_bucket'
8
- require File.dirname(__FILE__) + '/asset_cloud/blackhole_bucket'
9
- require File.dirname(__FILE__) + '/asset_cloud/memory_bucket'
10
- require File.dirname(__FILE__) + '/asset_cloud/file_system_bucket'
11
- require File.dirname(__FILE__) + '/asset_cloud/base'
7
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/active_record_bucket'
8
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/blackhole_bucket'
9
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/bucket_chain'
10
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/file_system_bucket'
11
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/invalid_bucket'
12
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/memory_bucket'
13
+ require File.dirname(__FILE__) + '/asset_cloud/buckets/versioned_memory_bucket'
14
+ require File.dirname(__FILE__) + '/asset_cloud/base'
12
15
 
13
16
 
14
17
  # Extensions
15
18
  require File.dirname(__FILE__) + '/asset_cloud/free_key_locator'
16
19
  require File.dirname(__FILE__) + '/asset_cloud/callbacks'
20
+ require File.dirname(__FILE__) + '/asset_cloud/validations'
17
21
 
18
22
 
19
23
  AssetCloud::Base.class_eval do
20
- include AssetCloud::FreeKeyLocator
21
- include AssetCloud::Callbacks
24
+ include AssetCloud::FreeKeyLocator
25
+ include AssetCloud::Callbacks
26
+ callback_methods :write, :delete
27
+ end
28
+
29
+ AssetCloud::Asset.class_eval do
30
+ include AssetCloud::Callbacks
31
+ callback_methods :store, :delete
32
+
33
+ include AssetCloud::Validations
22
34
  end
23
35
 
@@ -6,7 +6,7 @@ module AssetCloud
6
6
  class AssetNotSaved < AssetError
7
7
  end
8
8
 
9
- class Asset
9
+ class Asset
10
10
  include Comparable
11
11
  attr_accessor :key, :value, :cloud, :metadata
12
12
  attr_accessor :new_asset
@@ -65,6 +65,10 @@ module AssetCloud
65
65
 
66
66
  def created_at
67
67
  metadata.created_at
68
+ end
69
+
70
+ def updated_at
71
+ metadata.updated_at
68
72
  end
69
73
 
70
74
  def delete
@@ -110,10 +114,24 @@ module AssetCloud
110
114
  def url
111
115
  cloud.url_for key
112
116
  end
113
-
117
+
118
+ def bucket_name
119
+ @key.split('/').first
120
+ end
114
121
 
115
122
  def inspect
116
123
  "#<#{self.class.name}: #{key}>"
117
124
  end
125
+
126
+ # versioning
127
+
128
+ def rollback(version)
129
+ self.value = cloud.read_version(key, version)
130
+ self
131
+ end
132
+
133
+ def versions
134
+ cloud.versions(key)
135
+ end
118
136
  end
119
137
  end
@@ -5,7 +5,7 @@ module AssetCloud
5
5
  end
6
6
 
7
7
  class Base
8
- cattr_accessor :logger
8
+ cattr_accessor :logger
9
9
 
10
10
  VALID_PATHS = /^[a-z0-9][a-z0-9_\-\/]+([a-z0-9][\w\-\ \.]*\.\w{2,6})?$/i
11
11
 
@@ -73,10 +73,10 @@ module AssetCloud
73
73
  end
74
74
  end
75
75
 
76
- def asset_at(key)
77
- check_key_for_errors(key)
76
+ def asset_at(*args)
77
+ check_key_for_errors(args.first)
78
78
 
79
- asset_class_for(key).at(self, key)
79
+ asset_class_for(args.first).at(self, *args)
80
80
  end
81
81
 
82
82
  def move(source, destination)
@@ -145,12 +145,26 @@ module AssetCloud
145
145
  end
146
146
 
147
147
  def []=(key, value)
148
- write(key, value)
148
+ asset = self[key]
149
+ asset.value = value
150
+ asset.store
149
151
  end
150
152
 
151
153
  def [](key)
152
154
  asset_at(key)
153
155
  end
156
+
157
+ # versioning
158
+
159
+ def read_version(key, version)
160
+ logger.info { " [#{self.class.name}] Reading from #{key} at version #{version}" } if logger
161
+ bucket_for(key).read_version(key, version)
162
+ end
163
+
164
+ def versions(key)
165
+ logger.info { " [#{self.class.name}] Getting all versions for #{key}" } if logger
166
+ bucket_for(key).versions(key)
167
+ end
154
168
 
155
169
  protected
156
170
 
@@ -1,8 +1,8 @@
1
1
 
2
2
  module AssetCloud
3
3
  class AssetNotFoundError < StandardError
4
- def initialize(key_or_message, message=false)
5
- super(message ? key_or_message : "Could not find asset #{key_or_message.to_s.inspect}")
4
+ def initialize(key, version=nil)
5
+ super(version ? "Could not find version #{version} of asset #{key}" : "Could not find asset #{key}")
6
6
  end
7
7
  end
8
8
 
@@ -10,30 +10,6 @@ module AssetCloud
10
10
  attr_reader :name
11
11
  attr_accessor :cloud
12
12
 
13
- # returns a new Bucket class which writes to each given Bucket
14
- # but only uses the first one for reading
15
- def self.chain(*klasses)
16
- Class.new(self) do
17
- attr_reader :chained_buckets
18
- define_method 'initialize' do |cloud, name|
19
- super
20
- @chained_buckets = klasses.map {|klass| klass.new(cloud,name)}
21
- end
22
- def ls(key=nil)
23
- @chained_buckets.first.ls(key)
24
- end
25
- def read(key)
26
- @chained_buckets.first.read(key)
27
- end
28
- def write(key, data)
29
- @chained_buckets.each { |b| b.write(key, data)}
30
- end
31
- def delete(key)
32
- @chained_buckets.each { |b| b.delete(key)}
33
- end
34
- end
35
- end
36
-
37
13
  def initialize(cloud, name)
38
14
  @cloud, @name = cloud, name
39
15
  end
@@ -53,5 +29,15 @@ module AssetCloud
53
29
  def delete(key)
54
30
  raise NotImplementedError
55
31
  end
32
+
33
+ # versioning
34
+
35
+ def read_version(key, version)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def versions(key)
40
+ raise NotImplementedError
41
+ end
56
42
  end
57
43
  end
@@ -0,0 +1,57 @@
1
+ module AssetCloud
2
+ class ActiveRecordBucket < AssetCloud::Bucket
3
+ class_inheritable_accessor :key_attribute, :value_attribute
4
+ self.key_attribute = 'key'
5
+ self.value_attribute = 'value'
6
+
7
+ def ls(key=name)
8
+ col = records.connection.quote_column_name(self.key_attribute)
9
+ records.all(:conditions => ["#{col} LIKE ?", "#{key}%"]).map do |r|
10
+ cloud[r.send(self.key_attribute)]
11
+ end
12
+ end
13
+
14
+ def read(key)
15
+ find_record!(key).send(self.value_attribute)
16
+ end
17
+
18
+ def write(key, value)
19
+ record = records.send("find_or_initialize_by_#{self.key_attribute}", key.to_s)
20
+ record.send("#{self.value_attribute}=", value)
21
+ record.save!
22
+ end
23
+
24
+ def delete(key)
25
+ if record = find_record(key)
26
+ record.destroy
27
+ end
28
+ end
29
+
30
+ def stat(key)
31
+ if record = find_record(key)
32
+ AssetCloud::Metadata.new(true, record.send(self.value_attribute).size, record.created_at, record.updated_at)
33
+ else
34
+ AssetCloud::Metadata.new(false)
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ # override to return @cloud.user.assets or some other ActiveRecord Enumerable
41
+ # which responds to .connection, .find, etc.
42
+ #
43
+ # model must have columns for this class's key_attribute and value_attribute,
44
+ # plus created_at and updated_at.
45
+ def records
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def find_record(key)
50
+ records.first(:conditions => {self.key_attribute => key.to_s})
51
+ end
52
+
53
+ def find_record!(key)
54
+ find_record(key) or raise(AssetCloud::AssetNotFoundError, key)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,84 @@
1
+ module AssetCloud
2
+ class BucketChain < Bucket
3
+ # returns a new Bucket class which writes to each given Bucket
4
+ # but only uses the first one for reading
5
+ def self.chain(*klasses)
6
+ Class.new(self) do
7
+ attr_reader :chained_buckets
8
+ define_method 'initialize' do |cloud, name|
9
+ super
10
+ @chained_buckets = klasses.map {|klass| klass.new(cloud,name)}
11
+ end
12
+ end
13
+ end
14
+
15
+
16
+ def ls(key=nil)
17
+ first_possible_bucket {|b| b.ls(key)}
18
+ end
19
+ def read(key)
20
+ first_possible_bucket {|b| b.read(key)}
21
+ end
22
+ def stat(key=nil)
23
+ first_possible_bucket {|b| b.stat(key)}
24
+ end
25
+ def read_version(key, version)
26
+ first_possible_bucket {|b| b.read_version(key, version)}
27
+ end
28
+ def versions(key)
29
+ first_possible_bucket {|b| b.versions(key)}
30
+ end
31
+
32
+
33
+ def write(key, data)
34
+ every_bucket_with_transaction_on_key(key) {|b| b.write(key, data)}
35
+ end
36
+ def delete(key)
37
+ every_bucket_with_transaction_on_key(key) {|b| b.delete(key)}
38
+ end
39
+
40
+ def respond_to?(sym)
41
+ @chained_buckets.any? {|b| b.respond_to?(sym)}
42
+ end
43
+ def method_missing(sym, *args)
44
+ first_possible_bucket {|b| b.send(sym, *args)}
45
+ end
46
+
47
+ private
48
+
49
+ def first_possible_bucket(&block)
50
+ @chained_buckets.each do |bucket|
51
+ begin
52
+ return yield(bucket)
53
+ rescue NoMethodError, NotImplementedError => e
54
+ nil
55
+ end
56
+ end
57
+ end
58
+
59
+ def every_bucket_with_transaction_on_key(key, i=0, &block)
60
+ return unless bucket = @chained_buckets[i]
61
+
62
+ old_value = begin
63
+ bucket.read(key)
64
+ rescue AssetCloud::AssetNotFoundError
65
+ nil
66
+ end
67
+ result = yield(bucket)
68
+
69
+ begin
70
+ every_bucket_with_transaction_on_key(key, i+1, &block)
71
+ return result
72
+ rescue StandardError => e
73
+ if old_value
74
+ bucket.write(key, old_value)
75
+ else
76
+ bucket.delete(key)
77
+ end
78
+ raise e
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -8,7 +8,7 @@ module AssetCloud
8
8
 
9
9
  Dir.glob(base_path).each do |f|
10
10
  next unless File.file?(f)
11
- objects.push Asset.at(cloud, relative_path_for(f) )
11
+ objects.push cloud[relative_path_for(f)]
12
12
  end
13
13
  objects
14
14
  end
@@ -6,11 +6,13 @@ module AssetCloud
6
6
  super
7
7
  @memory = {}
8
8
  end
9
-
10
- def ls(key = nil)
11
- @memory.find_all do |key, value|
12
- key.left(key.size) == namespace
9
+
10
+ def ls(prefix=nil)
11
+ results = []
12
+ @memory.each do |k,v|
13
+ results.push(cloud[k]) if prefix.nil? || k.starts_with?(prefix)
13
14
  end
15
+ results
14
16
  end
15
17
 
16
18
  def read(key)
@@ -31,7 +33,7 @@ module AssetCloud
31
33
  def stat(key)
32
34
  return Metadata.non_existing unless @memory.has_key?(key)
33
35
 
34
- Metadata.new(true, @memory[key].size)
36
+ Metadata.new(true, read(key).size)
35
37
  end
36
38
 
37
39
  end
@@ -0,0 +1,33 @@
1
+ module AssetCloud
2
+
3
+ class VersionedMemoryBucket < MemoryBucket
4
+
5
+ def read(key)
6
+ raise AssetCloud::AssetNotFoundError, key unless @memory.has_key?(key)
7
+ read_version(key, latest_version(key))
8
+ end
9
+
10
+ def write(key, data)
11
+ @memory[key] ||= []
12
+ @memory[key] << data
13
+ true
14
+ end
15
+
16
+ def read_version(key, version)
17
+ @memory[key][version - 1]
18
+ end
19
+
20
+ def versions(key)
21
+ (1..latest_version(key)).to_a
22
+ end
23
+
24
+ private
25
+
26
+ def latest_version(key)
27
+ @memory[key].size
28
+ end
29
+
30
+ end
31
+
32
+
33
+ end
@@ -2,37 +2,40 @@ module AssetCloud
2
2
 
3
3
  module Callbacks
4
4
 
5
- CALLBACKS = [:delete, :write]
6
-
7
5
  def self.included(base)
8
-
9
- CALLBACKS.each do |method|
10
- code = <<-"end_eval"
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def callback_methods(*symbols)
11
+ symbols.each do |method|
12
+ code = <<-"end_eval"
11
13
 
12
- def self.before_#{method}(*callbacks, &block)
13
- callbacks << block if block_given?
14
- write_inheritable_array(:before_#{method}, callbacks)
15
- end
14
+ def self.before_#{method}(*callbacks, &block)
15
+ callbacks << block if block_given?
16
+ write_inheritable_array(:before_#{method}, callbacks)
17
+ end
16
18
 
17
- def self.after_#{method}(*callbacks, &block)
18
- callbacks << block if block_given?
19
- write_inheritable_array(:after_#{method}, callbacks)
20
- end
19
+ def self.after_#{method}(*callbacks, &block)
20
+ callbacks << block if block_given?
21
+ write_inheritable_array(:after_#{method}, callbacks)
22
+ end
21
23
 
22
24
 
23
- def #{method}_with_callbacks(*args)
24
- if execute_callbacks(:before_#{method}, args)
25
- result = #{method}_without_callbacks(*args)
26
- execute_callbacks(:after_#{method}, args)
27
- end
28
- result
29
- end
25
+ def #{method}_with_callbacks(*args)
26
+ if execute_callbacks(:before_#{method}, args)
27
+ result = #{method}_without_callbacks(*args)
28
+ execute_callbacks(:after_#{method}, args)
29
+ end
30
+ result
31
+ end
30
32
 
31
- alias_method_chain :#{method}, 'callbacks'
32
- end_eval
33
+ alias_method_chain :#{method}, 'callbacks'
34
+ end_eval
33
35
 
34
- base.class_eval code, __FILE__, __LINE__
35
- end
36
+ self.class_eval code, __FILE__, __LINE__
37
+ end
38
+ end
36
39
  end
37
40
 
38
41
  def execute_callbacks(symbol, args)
@@ -0,0 +1,43 @@
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 valid?
29
+ validate
30
+ errors.empty?
31
+ end
32
+
33
+ def add_error(msg)
34
+ errors << msg
35
+ errors.uniq!
36
+ end
37
+
38
+ def validate
39
+ errors.clear
40
+ execute_callbacks(:validate, [])
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,95 @@
1
+ require File.dirname(__FILE__) + '/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 = mock("connection"))
29
+ @mock_connection.should_receive(:quote_column_name).with('name').and_return("`name`")
30
+ (@mock_record = mock("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 = mock("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 = mock("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 = mock("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 = mock("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
data/spec/asset_spec.rb CHANGED
@@ -37,6 +37,10 @@ describe "Asset" do
37
37
  it "should have an ext" do
38
38
  @asset.extname.should == '.txt'
39
39
  end
40
+
41
+ it "should have a bucket_name" do
42
+ @asset.bucket_name.should == 'products'
43
+ end
40
44
 
41
45
 
42
46
  it "should store data to the bucket" do
data/spec/base_spec.rb CHANGED
@@ -64,6 +64,27 @@ describe BasicCloud do
64
64
  end
65
65
  end
66
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 = mock(: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
+
67
88
  describe "#bucket" do
68
89
  it "should allow specifying a class to use for assets in this bucket" do
69
90
  @fs['assets/rails_logo.gif'].should be_instance_of(AssetCloud::Asset)
@@ -0,0 +1,158 @@
1
+ require File.dirname(__FILE__) + '/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
@@ -1,9 +1,17 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper'
2
2
 
3
+ class CallbackAsset < AssetCloud::Asset
4
+ before_store :callback_before_store
5
+ after_delete :callback_after_delete
6
+ end
7
+
8
+ class BasicCloud < AssetCloud::Base
9
+ bucket :callback_assets, AssetCloud::MemoryBucket, :asset_class => CallbackAsset
10
+ end
11
+
3
12
  class CallbackCloud < AssetCloud::Base
4
13
  bucket :tmp, AssetCloud::MemoryBucket
5
14
 
6
-
7
15
  after_delete :callback_after_delete
8
16
  before_delete :callback_before_delete
9
17
 
@@ -75,4 +83,26 @@ describe MethodRecordingCloud do
75
83
  @fs['tmp/file.txt'] = 'random data'
76
84
  @fs.run_callbacks.should == [:callback_before_write, :callback_before_write]
77
85
  end
86
+ end
87
+
88
+ describe CallbackAsset do
89
+ before(:each) do
90
+ @fs = BasicCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
91
+ @fs.write('callback_assets/foo', 'bar')
92
+ @asset = @fs.asset_at('callback_assets/foo')
93
+ end
94
+
95
+ it "should run its before_store callback before store is called" do
96
+ @asset.should_receive(:callback_before_store).and_return(true)
97
+ @asset.should_not_receive(:callback_after_delete)
98
+
99
+ @asset.store
100
+ end
101
+
102
+ it "should run its after_delete callback after delete is called" do
103
+ @asset.should_not_receive(:callback_before_store)
104
+ @asset.should_receive(:callback_after_delete).and_return(true)
105
+
106
+ @asset.delete
107
+ end
78
108
  end
@@ -0,0 +1 @@
1
+ four
@@ -25,6 +25,22 @@ describe AssetCloud::MemoryBucket do
25
25
 
26
26
  end
27
27
 
28
+ describe '#ls' do
29
+ before do
30
+ %w{a b}.each do |letter|
31
+ 2.times {|number| @fs.write("memory/#{letter}#{number}",'.')}
32
+ end
33
+ end
34
+
35
+ it "should return a list of assets which start with the given prefix" do
36
+ @fs.buckets[:memory].ls('memory/a').size.should == 2
37
+ end
38
+
39
+ it "should return a list of all assets when a prefix is not given" do
40
+ @fs.buckets[:memory].ls.size.should == 4
41
+ end
42
+ end
43
+
28
44
 
29
45
 
30
46
  end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class ValidatedAsset < AssetCloud::Asset
4
+ validate :no_cats
5
+
6
+ private
7
+ def no_cats
8
+ add_error('no cats allowed!') if value =~ /cat/i
9
+ end
10
+ end
11
+
12
+ class BasicCloud < AssetCloud::Base
13
+ bucket :dog_pound, AssetCloud::MemoryBucket, :asset_class => ValidatedAsset
14
+ end
15
+
16
+ describe ValidatedAsset do
17
+ before(:each) do
18
+ @cloud = BasicCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
19
+ @cat = @cloud.build('dog_pound/fido', 'cat')
20
+ @dog = @cloud.build('dog_pound/fido', 'dog')
21
+ end
22
+
23
+ describe "#store" do
24
+ it "should not store the asset unless validations pass" do
25
+ @cloud.should_receive(:write).with('dog_pound/fido', 'dog').and_return(true)
26
+
27
+ @cat.store
28
+ @cat.store.should == false
29
+ @cat.errors.should == ['no cats allowed!']
30
+ @dog.store.should == true
31
+ end
32
+ end
33
+
34
+ describe "#valid?" do
35
+ it "should clear errors, run validations, and return validity" do
36
+ @cat.store
37
+ @cat.errors.should == ['no cats allowed!']
38
+ @cat.valid?.should == false
39
+ @cat.errors.should == ['no cats allowed!']
40
+ @cat.value = 'disguised feline'
41
+ @cat.valid?.should == true
42
+ @cat.errors.should be_empty
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class VersionedMemoryCloud < AssetCloud::Base
4
+ bucket :memory, AssetCloud::VersionedMemoryBucket
5
+ end
6
+
7
+ describe AssetCloud::VersionedMemoryBucket do
8
+ directory = File.dirname(__FILE__) + '/files'
9
+
10
+ before do
11
+ @fs = VersionedMemoryCloud.new(directory , 'http://assets/files' )
12
+ %w{one two three}.each do |content|
13
+ @fs.write("memory/foo", content)
14
+ end
15
+ end
16
+
17
+ describe '#read_version' do
18
+ it "should return the appropriate data when given a key and version" do
19
+ @fs.read_version('memory/foo', 1).should == 'one'
20
+ @fs.read_version('memory/foo', 3).should == 'three'
21
+ end
22
+ end
23
+
24
+ describe '#versions' do
25
+ it "should return a list of available version identifiers for the given key" do
26
+ @fs.versions('memory/foo').should == [1,2,3]
27
+ end
28
+ end
29
+
30
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jamesmacaulay-asset_cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-17 00:00:00 -07:00
12
+ date: 2009-07-03 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -36,27 +36,35 @@ files:
36
36
  - lib/asset_cloud.rb
37
37
  - lib/asset_cloud/asset.rb
38
38
  - lib/asset_cloud/base.rb
39
- - lib/asset_cloud/blackhole_bucket.rb
40
39
  - lib/asset_cloud/bucket.rb
40
+ - lib/asset_cloud/buckets/active_record_bucket.rb
41
+ - lib/asset_cloud/buckets/blackhole_bucket.rb
42
+ - lib/asset_cloud/buckets/bucket_chain.rb
43
+ - lib/asset_cloud/buckets/file_system_bucket.rb
44
+ - lib/asset_cloud/buckets/invalid_bucket.rb
45
+ - lib/asset_cloud/buckets/memory_bucket.rb
46
+ - lib/asset_cloud/buckets/versioned_memory_bucket.rb
41
47
  - lib/asset_cloud/callbacks.rb
42
- - lib/asset_cloud/file_system_bucket.rb
43
48
  - lib/asset_cloud/free_key_locator.rb
44
- - lib/asset_cloud/invalid_bucket.rb
45
- - lib/asset_cloud/memory_bucket.rb
46
49
  - lib/asset_cloud/metadata.rb
50
+ - lib/asset_cloud/validations.rb
51
+ - spec/active_record_bucket_spec.rb
47
52
  - spec/asset_spec.rb
48
53
  - spec/base_spec.rb
49
54
  - spec/blackhole_bucket_spec.rb
50
- - spec/bucket_spec.rb
55
+ - spec/bucket_chain_spec.rb
51
56
  - spec/callbacks_spec.rb
52
57
  - spec/file_system_spec.rb
53
58
  - spec/files/products/key.txt
59
+ - spec/files/versioned_stuff/foo
54
60
  - spec/find_free_key_spec.rb
55
61
  - spec/memory_bucket_spec.rb
56
62
  - spec/regexp_spec.rb
57
63
  - spec/spec.opts
58
64
  - spec/spec_helper.rb
59
- has_rdoc: true
65
+ - spec/validations_spec.rb
66
+ - spec/versioned_memory_bucket_spec.rb
67
+ has_rdoc: false
60
68
  homepage: http://github.com/Shopify/asset_cloud
61
69
  post_install_message:
62
70
  rdoc_options:
@@ -83,13 +91,16 @@ signing_key:
83
91
  specification_version: 3
84
92
  summary: An abstraction layer around arbitrary and diverse asset stores.
85
93
  test_files:
94
+ - spec/active_record_bucket_spec.rb
86
95
  - spec/asset_spec.rb
87
96
  - spec/base_spec.rb
88
97
  - spec/blackhole_bucket_spec.rb
89
- - spec/bucket_spec.rb
98
+ - spec/bucket_chain_spec.rb
90
99
  - spec/callbacks_spec.rb
91
100
  - spec/file_system_spec.rb
92
101
  - spec/find_free_key_spec.rb
93
102
  - spec/memory_bucket_spec.rb
94
103
  - spec/regexp_spec.rb
95
104
  - spec/spec_helper.rb
105
+ - spec/validations_spec.rb
106
+ - spec/versioned_memory_bucket_spec.rb
data/spec/bucket_spec.rb DELETED
@@ -1,41 +0,0 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
2
-
3
- class ChainedCloud < AssetCloud::Base
4
- bucket :stuff, AssetCloud::Bucket.chain(AssetCloud::MemoryBucket, AssetCloud::BlackholeBucket)
5
- end
6
-
7
- describe AssetCloud::Bucket do
8
- directory = File.dirname(__FILE__) + '/files'
9
-
10
- before(:each) do
11
- @cloud = ChainedCloud.new(directory , 'http://assets/files' )
12
- @bucket_chain = @cloud.buckets[:stuff]
13
- @memory_bucket, @blackhole_bucket = @bucket_chain.chained_buckets
14
- end
15
-
16
- describe "#chain" do
17
- it 'should take multiple Bucket classes and return a new Bucket class' do
18
- @bucket_chain.should be_a_kind_of(AssetCloud::Bucket)
19
- end
20
-
21
- it 'should return a Bucket which writes to each sub-bucket' do
22
- @bucket_chain.chained_buckets.each do |bucket|
23
- bucket.should_receive(:write).with('stuff/foo', 'bar').and_return(true)
24
- bucket.should_receive(:delete).with('stuff/foo').and_return(true)
25
- end
26
-
27
- @bucket_chain.write('stuff/foo', 'bar')
28
- @bucket_chain.delete('stuff/foo')
29
- end
30
-
31
- it 'should return a Bucket which reads from only the first sub-bucket' do
32
- @memory_bucket.should_receive(:read).with('stuff/foo').and_return('bar')
33
- @memory_bucket.should_receive(:ls).with(nil).and_return(:some_assets)
34
- @blackhole_bucket.should_not_receive(:read)
35
- @blackhole_bucket.should_not_receive(:ls)
36
-
37
- @bucket_chain.read('stuff/foo').should == 'bar'
38
- @bucket_chain.ls.should == :some_assets
39
- end
40
- end
41
- end