jamesmacaulay-asset_cloud 0.5.1 → 0.5.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.
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