bulldog 0.0.1

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 (77) hide show
  1. data/.gitignore +2 -0
  2. data/DESCRIPTION.txt +3 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +64 -0
  6. data/VERSION +1 -0
  7. data/bulldog.gemspec +157 -0
  8. data/lib/bulldog.rb +95 -0
  9. data/lib/bulldog/attachment.rb +49 -0
  10. data/lib/bulldog/attachment/base.rb +167 -0
  11. data/lib/bulldog/attachment/has_dimensions.rb +94 -0
  12. data/lib/bulldog/attachment/image.rb +63 -0
  13. data/lib/bulldog/attachment/maybe.rb +229 -0
  14. data/lib/bulldog/attachment/none.rb +37 -0
  15. data/lib/bulldog/attachment/pdf.rb +63 -0
  16. data/lib/bulldog/attachment/unknown.rb +11 -0
  17. data/lib/bulldog/attachment/video.rb +143 -0
  18. data/lib/bulldog/error.rb +5 -0
  19. data/lib/bulldog/has_attachment.rb +214 -0
  20. data/lib/bulldog/interpolation.rb +73 -0
  21. data/lib/bulldog/missing_file.rb +12 -0
  22. data/lib/bulldog/processor.rb +5 -0
  23. data/lib/bulldog/processor/argument_tree.rb +116 -0
  24. data/lib/bulldog/processor/base.rb +124 -0
  25. data/lib/bulldog/processor/ffmpeg.rb +172 -0
  26. data/lib/bulldog/processor/image_magick.rb +134 -0
  27. data/lib/bulldog/processor/one_shot.rb +19 -0
  28. data/lib/bulldog/reflection.rb +234 -0
  29. data/lib/bulldog/saved_file.rb +19 -0
  30. data/lib/bulldog/stream.rb +186 -0
  31. data/lib/bulldog/style.rb +38 -0
  32. data/lib/bulldog/style_set.rb +101 -0
  33. data/lib/bulldog/tempfile.rb +28 -0
  34. data/lib/bulldog/util.rb +92 -0
  35. data/lib/bulldog/validations.rb +68 -0
  36. data/lib/bulldog/vector2.rb +18 -0
  37. data/rails/init.rb +9 -0
  38. data/script/console +8 -0
  39. data/spec/data/empty.txt +0 -0
  40. data/spec/data/test.jpg +0 -0
  41. data/spec/data/test.mov +0 -0
  42. data/spec/data/test.pdf +0 -0
  43. data/spec/data/test.png +0 -0
  44. data/spec/data/test2.jpg +0 -0
  45. data/spec/helpers/image_creation.rb +8 -0
  46. data/spec/helpers/temporary_directory.rb +25 -0
  47. data/spec/helpers/temporary_models.rb +76 -0
  48. data/spec/helpers/temporary_values.rb +102 -0
  49. data/spec/helpers/test_upload_files.rb +108 -0
  50. data/spec/helpers/time_travel.rb +20 -0
  51. data/spec/integration/data/test.jpg +0 -0
  52. data/spec/integration/lifecycle_hooks_spec.rb +213 -0
  53. data/spec/integration/processing_image_attachments.rb +72 -0
  54. data/spec/integration/processing_video_attachments_spec.rb +82 -0
  55. data/spec/integration/saving_an_attachment_spec.rb +31 -0
  56. data/spec/matchers/file_operations.rb +159 -0
  57. data/spec/spec_helper.rb +76 -0
  58. data/spec/unit/attachment/base_spec.rb +311 -0
  59. data/spec/unit/attachment/image_spec.rb +128 -0
  60. data/spec/unit/attachment/maybe_spec.rb +126 -0
  61. data/spec/unit/attachment/pdf_spec.rb +137 -0
  62. data/spec/unit/attachment/video_spec.rb +176 -0
  63. data/spec/unit/attachment_spec.rb +61 -0
  64. data/spec/unit/has_attachment_spec.rb +700 -0
  65. data/spec/unit/interpolation_spec.rb +108 -0
  66. data/spec/unit/processor/argument_tree_spec.rb +159 -0
  67. data/spec/unit/processor/ffmpeg_spec.rb +467 -0
  68. data/spec/unit/processor/image_magick_spec.rb +260 -0
  69. data/spec/unit/processor/one_shot_spec.rb +70 -0
  70. data/spec/unit/reflection_spec.rb +338 -0
  71. data/spec/unit/stream_spec.rb +234 -0
  72. data/spec/unit/style_set_spec.rb +44 -0
  73. data/spec/unit/style_spec.rb +51 -0
  74. data/spec/unit/validations_spec.rb +491 -0
  75. data/spec/unit/vector2_spec.rb +27 -0
  76. data/tasks/bulldog_tasks.rake +4 -0
  77. metadata +193 -0
@@ -0,0 +1,102 @@
1
+ module TemporaryValues
2
+ def self.included(mod)
3
+ mod.extend ClassMethods
4
+ end
5
+
6
+ #
7
+ # Use +value+ for the value of the constant +mod::name+ for the
8
+ # duration of the block.
9
+ #
10
+ def with_temporary_constant_value(mod, name, value)
11
+ save_and_set_constant(mod, name, value)
12
+ yield
13
+ ensure
14
+ restore_constant(mod, name)
15
+ end
16
+
17
+ #
18
+ # Use +value+ for the value of the +name+ attribute of the given
19
+ # +object+ for the duration of the block.
20
+ #
21
+ def with_temporary_attribute_value(object, name, value)
22
+ save_and_set_attribute(object, name, value)
23
+ yield
24
+ ensure
25
+ restore_attribute(object, name)
26
+ end
27
+
28
+ module ClassMethods
29
+ #
30
+ # Use +value+ for the value of the constant +mod::name+ for the
31
+ # duration of each example in this example group.
32
+ #
33
+ # If the value should be recalculated for each example, pass a
34
+ # block instead of a value argument.
35
+ #
36
+ def use_temporary_constant_value(mod, name, value=nil, &block)
37
+ before do
38
+ value = block ? instance_eval(&block) : value
39
+ save_and_set_constant(mod, name, value)
40
+ end
41
+ after do
42
+ restore_constant(mod, name)
43
+ end
44
+ end
45
+
46
+ #
47
+ # Use +value+ for the value of the +name+ attribute of the given
48
+ # +object+ for the duration of each example in this example group.
49
+ #
50
+ # If the value should be recalculated for each example, pass a
51
+ # block instead of a value argument.
52
+ #
53
+ def use_temporary_attribute_value(object, name, value=nil, &block)
54
+ before do
55
+ value = block ? instance_eval(&block) : value
56
+ save_and_set_attribute(object, name, value)
57
+ end
58
+ after do
59
+ restore_attribute(object, name)
60
+ end
61
+ end
62
+ end
63
+
64
+ private # ---------------------------------------------------------
65
+
66
+ UNDEFINED = Object.new
67
+
68
+ def original_attribute_values
69
+ @original_attribute_values ||= Hash.new{|h,k| h[k] = Hash.new{|h2,k2| h2[k2] = {}}}
70
+ end
71
+
72
+ def save_and_set_attribute(object, name, value)
73
+ original_attribute_values[object.__id__][name] = object.send(name)
74
+ object.send("#{name}=", value)
75
+ end
76
+
77
+ def restore_attribute(object, name)
78
+ value = original_attribute_values[object.__id__].delete(name)
79
+ object.send("#{name}=", value)
80
+ end
81
+
82
+ def original_constant_values
83
+ @original_constant_values ||= Hash.new{|h,k| h[k] = Hash.new{|h2,k2| h2[k2] = {}}}
84
+ end
85
+
86
+ def save_and_set_constant(mod, name, value)
87
+ if mod.const_defined?(name)
88
+ original_constant_values[mod.name][name] = mod.send(:remove_const, name)
89
+ else
90
+ original_constant_values[mod.name][name] = UNDEFINED
91
+ end
92
+ mod.const_set(name, value)
93
+ end
94
+
95
+ def restore_constant(mod, name)
96
+ mod.send(:remove_const, name)
97
+ original_value = original_constant_values[mod.name].delete(name)
98
+ unless original_value.equal?(UNDEFINED)
99
+ mod.const_set(name, original_value)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,108 @@
1
+ module TestUploadFiles
2
+ def self.included(mod)
3
+ mod.before{init_test_upload_files}
4
+ mod.after{close_test_upload_files}
5
+ end
6
+
7
+ def autoclose(file)
8
+ @files_to_close << file
9
+ file
10
+ end
11
+
12
+ def small_uploaded_file(path=unused_temp_path, test_file='test.jpg')
13
+ content = content_for_uploaded_file(test_file)
14
+ io = ActionController::UploadedStringIO.new(content)
15
+ io.original_path = path
16
+ io.content_type = Rack::Mime::MIME_TYPES[File.extname(path)]
17
+ io
18
+ end
19
+
20
+ def large_uploaded_file(path=unused_temp_path, test_file='test.jpg')
21
+ content = content_for_uploaded_file(test_file)
22
+ file = ActionController::UploadedTempfile.new(path)
23
+ file.write(content)
24
+ file.rewind
25
+ file.original_path = path
26
+ file.content_type = Rack::Mime::MIME_TYPES[File.extname(path)]
27
+ autoclose(file)
28
+ end
29
+
30
+ def uploaded_file_with_content(basename, content)
31
+ io = ActionController::UploadedStringIO.new(content)
32
+ io.original_path = basename
33
+ io.content_type = Rack::Mime::MIME_TYPES[File.extname(basename)]
34
+ io
35
+ end
36
+
37
+ def test_file(basename)
38
+ path = test_path(basename)
39
+ file = open(path)
40
+ autoclose(file)
41
+ end
42
+
43
+ def test_path(basename)
44
+ "#{ROOT}/spec/data/#{basename}"
45
+ end
46
+
47
+ # For when it doesn't matter if it's small or large.
48
+ alias uploaded_file small_uploaded_file
49
+
50
+ def test_image_file(basename='test.jpg')
51
+ path = test_image_path(basename)
52
+ file = open(path)
53
+ autoclose(file)
54
+ end
55
+
56
+ def test_image_path(basename='test.jpg')
57
+ test_path(basename)
58
+ end
59
+
60
+ def test_video_file(basename='test.mov')
61
+ path = test_video_path(basename)
62
+ file = open(path)
63
+ autoclose(file)
64
+ end
65
+
66
+ def test_video_path(basename='test.mov')
67
+ test_path(basename)
68
+ end
69
+
70
+ def test_pdf_file(basename='test.pdf')
71
+ path = test_pdf_path(basename)
72
+ file = open(path)
73
+ autoclose(file)
74
+ end
75
+
76
+ def test_pdf_path(basename='test.pdf')
77
+ test_path(basename)
78
+ end
79
+
80
+ def test_empty_file
81
+ file = open(test_path('empty.txt'))
82
+ autoclose(file)
83
+ end
84
+
85
+ private # ---------------------------------------------------------
86
+
87
+ def init_test_upload_files
88
+ @files_to_close = []
89
+ end
90
+
91
+ def close_test_upload_files
92
+ @files_to_close.each(&:close)
93
+ end
94
+
95
+ def content_for_uploaded_file(test_file)
96
+ path = test_path("test#{File.extname(test_file)}")
97
+ File.read(path)
98
+ end
99
+
100
+ def unused_temp_path
101
+ i = 0
102
+ path = nil
103
+ begin
104
+ path = File.join(temporary_directory, "test-#{i}.jpg")
105
+ end while File.exist?(path)
106
+ path
107
+ end
108
+ end
@@ -0,0 +1,20 @@
1
+ module TimeTravel
2
+ def self.included(mod)
3
+ mod.before{stop_time}
4
+ end
5
+
6
+ def stop_time
7
+ warp_to Time.now
8
+ end
9
+
10
+ def warp_to(time)
11
+ time = Time.parse(time) if time.is_a?(String)
12
+ Time.stubs(:now).returns(time)
13
+ end
14
+
15
+ def warp_ahead(duration)
16
+ new_now = Time.now + duration
17
+ Time.stubs(:now).returns(new_now)
18
+ new_now
19
+ end
20
+ end
@@ -0,0 +1,213 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Lifecycle hooks" do
4
+ use_model_class(:Thing,
5
+ :attachment_file_name => :string,
6
+ :value => :integer)
7
+
8
+ before do
9
+ @file = open("#{ROOT}/spec/integration/data/test.jpg")
10
+ Bulldog.default_url_template = "/attachments/:class/:id.:style.:extension"
11
+ end
12
+
13
+ after do
14
+ @file.close
15
+ end
16
+
17
+ it "should run before-validation hooks before validating the record" do
18
+ checks = []
19
+ Thing.validates_presence_of :value
20
+ Thing.has_attachment :attachment do
21
+ style :normal
22
+ process :before => :validation do
23
+ checks << record.errors.empty?
24
+ end
25
+ end
26
+ thing = Thing.new(:attachment => @file)
27
+ checks.should == []
28
+ thing.valid?.should be_false
29
+ checks.should == [true]
30
+ end
31
+
32
+ it "should run after-validation hooks after validating the record" do
33
+ checks = []
34
+ Thing.validates_presence_of :value
35
+ Thing.has_attachment :attachment do
36
+ style :normal
37
+ process :after => :validation do
38
+ checks << record.errors.empty?
39
+ end
40
+ end
41
+ thing = Thing.new(:attachment => @file)
42
+ checks.should == []
43
+ thing.valid?.should be_false
44
+ checks.should == [false]
45
+ end
46
+
47
+ it "should run before-save hooks before saving the record" do
48
+ checks = []
49
+ Thing.has_attachment :attachment do
50
+ style :normal
51
+ process :before => :save do
52
+ checks << record.new_record?
53
+ end
54
+ end
55
+ thing = Thing.new(:attachment => @file)
56
+ checks.should == []
57
+ thing.save.should be_true
58
+ checks.should == [true]
59
+ end
60
+
61
+ it "should run after-save hooks after saving the record" do
62
+ checks = []
63
+ Thing.has_attachment :attachment do
64
+ style :normal
65
+ process :after => :save do
66
+ checks << record.new_record?
67
+ end
68
+ end
69
+ thing = Thing.new(:attachment => @file)
70
+ checks.should == []
71
+ thing.save.should be_true
72
+ checks.should == [false]
73
+ end
74
+
75
+ it "should run before-create hooks before creating the record" do
76
+ checks = []
77
+ Thing.has_attachment :attachment do
78
+ style :normal
79
+ process :before => :create do
80
+ checks << record.new_record?
81
+ end
82
+ end
83
+ thing = Thing.new(:attachment => @file)
84
+ checks.should == []
85
+ thing.save.should be_true
86
+ checks.should == [true]
87
+ end
88
+
89
+ it "should run after-create hooks after creating the record" do
90
+ checks = []
91
+ Thing.has_attachment :attachment do
92
+ style :normal
93
+ process :after => :create do
94
+ checks << record.new_record?
95
+ end
96
+ end
97
+ thing = Thing.new(:attachment => @file)
98
+ checks.should == []
99
+ thing.save.should be_true
100
+ checks.should == [false]
101
+ end
102
+
103
+ it "should run before-update hooks before updating the record" do
104
+ checks = []
105
+ Thing.has_attachment :attachment do
106
+ style :normal
107
+ process :before => :update do
108
+ checks << Thing.count(:conditions => {:value => 2})
109
+ end
110
+ end
111
+ Thing.create(:attachment => @file, :value => 1)
112
+ thing = Thing.first
113
+ checks.should == []
114
+ thing.update_attributes(:value => 2).should be_true
115
+ checks.should == [0]
116
+ end
117
+
118
+ it "should run after-update hooks after updating the record" do
119
+ checks = []
120
+ Thing.has_attachment :attachment do
121
+ style :normal
122
+ process :after => :update do
123
+ checks << Thing.count(:conditions => {:value => 2})
124
+ end
125
+ end
126
+ Thing.create(:attachment => @file, :value => 1)
127
+ thing = Thing.first
128
+ checks.should == []
129
+ thing.update_attributes(:value => 2).should be_true
130
+ checks.should == [1]
131
+ end
132
+
133
+ it "should not run create hooks when updating the record" do
134
+ checks = []
135
+ Thing.has_attachment :attachment do
136
+ style :normal
137
+ process :before => :update do
138
+ checks << [:fail]
139
+ end
140
+
141
+ process :after => :update do |thing|
142
+ checks << [:fail]
143
+ end
144
+ end
145
+ Thing.create(:attachment => @file)
146
+ checks.should == []
147
+ end
148
+
149
+ it "should not run update hooks when creating the record" do
150
+ checks = []
151
+ Thing.has_attachment :attachment do
152
+ style :normal
153
+ process :before => :create do
154
+ checks << [:fail]
155
+ end
156
+
157
+ process :after => :create do
158
+ checks << [:fail]
159
+ end
160
+ end
161
+ thing = Thing.create(:attachment => @file, :value => 1)
162
+ checks = []
163
+ thing.update_attributes(:value => 2)
164
+ checks.should == []
165
+ end
166
+
167
+ it "should run callbacks for the given attachment types if given" do
168
+ runs = 0
169
+ Thing.has_attachment :attachment do
170
+ style :normal
171
+ process(:types => :image, :on => :test_event){runs += 1}
172
+ end
173
+ thing = Thing.new(:attachment => @file)
174
+ thing.process_attachment(:attachment, :test_event)
175
+ runs.should == 1
176
+ end
177
+
178
+ it "should not run callbacks if the attachment is of the wrong type" do
179
+ runs = 0
180
+ Thing.has_attachment :attachment do
181
+ style :normal
182
+ process(:types => :video, :on => :test_event){runs += 1}
183
+ end
184
+ thing = Thing.new(:attachment => @file)
185
+ thing.process_attachment(:attachment, :test_event)
186
+ runs.should == 0
187
+ end
188
+
189
+ it "should allow specifying more than one type" do
190
+ runs = 0
191
+ Thing.has_attachment :attachment do
192
+ style :normal
193
+ process(:types => [:image, :video], :on => :test_event){runs += 1}
194
+ end
195
+ thing = Thing.new(:attachment => @file)
196
+ thing.process_attachment(:attachment, :test_event)
197
+ thing.attachment = uploaded_file_with_content('test.avi', "RIFF AVI ")
198
+ thing.process_attachment(:attachment, :test_event)
199
+ runs.should == 2
200
+ end
201
+
202
+ it "should run multiple callbacks if given" do
203
+ checks = []
204
+ Thing.has_attachment :attachment do
205
+ style :normal
206
+ process(:on => :test_event){checks << 1}
207
+ process(:on => :test_event){checks << 2}
208
+ end
209
+ thing = Thing.new(:attachment => @file)
210
+ thing.process_attachment(:attachment, :test_event)
211
+ checks.should == [1, 2]
212
+ end
213
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Processing image attachments" do
4
+ use_model_class(:Thing,
5
+ :width => :integer,
6
+ :height => :integer)
7
+
8
+ def identify
9
+ Bulldog::Processor::ImageMagick.identify_command
10
+ end
11
+
12
+ before do
13
+ tmp = temporary_directory
14
+ Thing.has_attachment :photo do
15
+ path "#{tmp}/:id.:style.png"
16
+ style :small, {:size => '10x10!'}
17
+ style :large, {:size => '1000x1000!'}
18
+
19
+ process :before => :save do
20
+ width, height = dimensions
21
+ record.width = width
22
+ record.height = height
23
+ end
24
+
25
+ process :on => :resize do
26
+ resize
27
+ end
28
+ end
29
+ @thing = Thing.new
30
+ @thing.stubs(:id).returns(5)
31
+ @file = small_uploaded_file(:size => "40x30")
32
+ end
33
+
34
+ def small_uploaded_file(options={})
35
+ tmp_path = "#{temporary_directory}/small_uploaded_file.tmp.png"
36
+ create_image(tmp_path, options)
37
+ content = File.read(tmp_path)
38
+ File.unlink(tmp_path)
39
+
40
+ io = ActionController::UploadedStringIO.new(content)
41
+ io.original_path = "tmp.png"
42
+ io.content_type = 'image/png'
43
+ io
44
+ end
45
+
46
+ after do
47
+ @file.close
48
+ end
49
+
50
+ def path(style)
51
+ "#{temporary_directory}/5.#{style}.png"
52
+ end
53
+
54
+ it "should run the after_save process after saving" do
55
+ @thing.update_attributes(:photo => @file)
56
+ @thing.width.should == 40
57
+ @thing.height.should == 30
58
+ end
59
+
60
+ it "should process the attachment when the processor is run" do
61
+ File.exist?(path(:original)).should be_false
62
+ File.exist?(path(:small)).should be_false
63
+ File.exist?(path(:large)).should be_false
64
+
65
+ @thing.update_attributes(:photo => @file).should be_true
66
+ @thing.process_attachment(:photo, :resize)
67
+
68
+ `"#{identify}" -format "%w %h" "#{path(:original)}"`.chomp.should == '40 30'
69
+ `"#{identify}" -format "%w %h" "#{path(:small)}"`.chomp.should == '10 10'
70
+ `"#{identify}" -format "%w %h" "#{path(:large)}"`.chomp.should == '1000 1000'
71
+ end
72
+ end