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.
- data/.gitignore +2 -0
- data/DESCRIPTION.txt +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/bulldog.gemspec +157 -0
- data/lib/bulldog.rb +95 -0
- data/lib/bulldog/attachment.rb +49 -0
- data/lib/bulldog/attachment/base.rb +167 -0
- data/lib/bulldog/attachment/has_dimensions.rb +94 -0
- data/lib/bulldog/attachment/image.rb +63 -0
- data/lib/bulldog/attachment/maybe.rb +229 -0
- data/lib/bulldog/attachment/none.rb +37 -0
- data/lib/bulldog/attachment/pdf.rb +63 -0
- data/lib/bulldog/attachment/unknown.rb +11 -0
- data/lib/bulldog/attachment/video.rb +143 -0
- data/lib/bulldog/error.rb +5 -0
- data/lib/bulldog/has_attachment.rb +214 -0
- data/lib/bulldog/interpolation.rb +73 -0
- data/lib/bulldog/missing_file.rb +12 -0
- data/lib/bulldog/processor.rb +5 -0
- data/lib/bulldog/processor/argument_tree.rb +116 -0
- data/lib/bulldog/processor/base.rb +124 -0
- data/lib/bulldog/processor/ffmpeg.rb +172 -0
- data/lib/bulldog/processor/image_magick.rb +134 -0
- data/lib/bulldog/processor/one_shot.rb +19 -0
- data/lib/bulldog/reflection.rb +234 -0
- data/lib/bulldog/saved_file.rb +19 -0
- data/lib/bulldog/stream.rb +186 -0
- data/lib/bulldog/style.rb +38 -0
- data/lib/bulldog/style_set.rb +101 -0
- data/lib/bulldog/tempfile.rb +28 -0
- data/lib/bulldog/util.rb +92 -0
- data/lib/bulldog/validations.rb +68 -0
- data/lib/bulldog/vector2.rb +18 -0
- data/rails/init.rb +9 -0
- data/script/console +8 -0
- data/spec/data/empty.txt +0 -0
- data/spec/data/test.jpg +0 -0
- data/spec/data/test.mov +0 -0
- data/spec/data/test.pdf +0 -0
- data/spec/data/test.png +0 -0
- data/spec/data/test2.jpg +0 -0
- data/spec/helpers/image_creation.rb +8 -0
- data/spec/helpers/temporary_directory.rb +25 -0
- data/spec/helpers/temporary_models.rb +76 -0
- data/spec/helpers/temporary_values.rb +102 -0
- data/spec/helpers/test_upload_files.rb +108 -0
- data/spec/helpers/time_travel.rb +20 -0
- data/spec/integration/data/test.jpg +0 -0
- data/spec/integration/lifecycle_hooks_spec.rb +213 -0
- data/spec/integration/processing_image_attachments.rb +72 -0
- data/spec/integration/processing_video_attachments_spec.rb +82 -0
- data/spec/integration/saving_an_attachment_spec.rb +31 -0
- data/spec/matchers/file_operations.rb +159 -0
- data/spec/spec_helper.rb +76 -0
- data/spec/unit/attachment/base_spec.rb +311 -0
- data/spec/unit/attachment/image_spec.rb +128 -0
- data/spec/unit/attachment/maybe_spec.rb +126 -0
- data/spec/unit/attachment/pdf_spec.rb +137 -0
- data/spec/unit/attachment/video_spec.rb +176 -0
- data/spec/unit/attachment_spec.rb +61 -0
- data/spec/unit/has_attachment_spec.rb +700 -0
- data/spec/unit/interpolation_spec.rb +108 -0
- data/spec/unit/processor/argument_tree_spec.rb +159 -0
- data/spec/unit/processor/ffmpeg_spec.rb +467 -0
- data/spec/unit/processor/image_magick_spec.rb +260 -0
- data/spec/unit/processor/one_shot_spec.rb +70 -0
- data/spec/unit/reflection_spec.rb +338 -0
- data/spec/unit/stream_spec.rb +234 -0
- data/spec/unit/style_set_spec.rb +44 -0
- data/spec/unit/style_spec.rb +51 -0
- data/spec/unit/validations_spec.rb +491 -0
- data/spec/unit/vector2_spec.rb +27 -0
- data/tasks/bulldog_tasks.rake +4 -0
- metadata +193 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Bulldog
|
2
|
+
class Style
|
3
|
+
def initialize(name, attributes={})
|
4
|
+
@name = name
|
5
|
+
@attributes = attributes
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :name, :attributes
|
9
|
+
|
10
|
+
#
|
11
|
+
# Return the value of the given style attribute.
|
12
|
+
#
|
13
|
+
delegate :[], :to => :attributes
|
14
|
+
|
15
|
+
#
|
16
|
+
# Set the value of the given style attribute.
|
17
|
+
#
|
18
|
+
delegate :[]=, :to => :attributes
|
19
|
+
|
20
|
+
#
|
21
|
+
# Return true if the argument is a Style with the same name and
|
22
|
+
# attributes.
|
23
|
+
#
|
24
|
+
def ==(other)
|
25
|
+
other.is_a?(self.class) &&
|
26
|
+
name == other.name &&
|
27
|
+
attributes == other.attributes
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<Style #{name.inspect} #{attributes.inspect}>"
|
32
|
+
end
|
33
|
+
|
34
|
+
delegate :hash, :eql?, :to => :name
|
35
|
+
|
36
|
+
ORIGINAL = new(:original, {})
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Bulldog
|
2
|
+
#
|
3
|
+
# An ordered set of Styles.
|
4
|
+
#
|
5
|
+
# Lookup is by style name.
|
6
|
+
#
|
7
|
+
class StyleSet
|
8
|
+
#
|
9
|
+
# Create a StyleSet containing the given styles.
|
10
|
+
#
|
11
|
+
def initialize(styles=[])
|
12
|
+
@styles = styles.to_a
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Initialize a StyleSet from another.
|
17
|
+
#
|
18
|
+
def initialize_copy(other)
|
19
|
+
super
|
20
|
+
@styles = @styles.clone
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Create a StyleSet containing the given styles.
|
25
|
+
#
|
26
|
+
def self.[](*styles)
|
27
|
+
new(styles)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Return the style with the given name.
|
32
|
+
#
|
33
|
+
def [](arg)
|
34
|
+
if arg.is_a?(Symbol)
|
35
|
+
if arg == :original
|
36
|
+
Style::ORIGINAL
|
37
|
+
else
|
38
|
+
@styles.find{|style| style.name == arg}
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@styles[arg]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Add the given style to the set.
|
47
|
+
#
|
48
|
+
def <<(style)
|
49
|
+
@styles << style
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Return true if the given object has the same styles as this one.
|
54
|
+
#
|
55
|
+
# The argument must have #to_a defined.
|
56
|
+
#
|
57
|
+
def ==(other)
|
58
|
+
other.to_a == @styles
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Return the list of styles as an Array.
|
63
|
+
#
|
64
|
+
def to_a
|
65
|
+
@styles.dup
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Return true if there are no styles in the set, false otherwise.
|
70
|
+
#
|
71
|
+
def empty?
|
72
|
+
@styles.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Return the style with the given names.
|
77
|
+
#
|
78
|
+
def slice(*names)
|
79
|
+
styles = names.map{|name| self[name]}
|
80
|
+
StyleSet[*styles]
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Clear all styles out of the style set.
|
85
|
+
#
|
86
|
+
# The original will still be retrievable.
|
87
|
+
#
|
88
|
+
def clear
|
89
|
+
@styles.clear
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Yield each style.
|
94
|
+
#
|
95
|
+
def each(&block)
|
96
|
+
@styles.each(&block)
|
97
|
+
end
|
98
|
+
|
99
|
+
include Enumerable
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Bulldog
|
4
|
+
if RUBY_VERSION >= '1.8.7'
|
5
|
+
Tempfile = ::Tempfile
|
6
|
+
else
|
7
|
+
#
|
8
|
+
# Backport Ruby 1.8.7's Tempfile feature which allows specifying the
|
9
|
+
# extension by passing in the path as a 2-element array. Useful for
|
10
|
+
# things like ImageMagick, which often rely on the file extension.
|
11
|
+
#
|
12
|
+
class Tempfile < ::Tempfile
|
13
|
+
private
|
14
|
+
|
15
|
+
def make_tmpname(basename, n)
|
16
|
+
case basename
|
17
|
+
when Array
|
18
|
+
prefix, suffix = *basename
|
19
|
+
else
|
20
|
+
prefix, suffix = basename, ''
|
21
|
+
end
|
22
|
+
|
23
|
+
t = Time.now.strftime("%Y%m%d")
|
24
|
+
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}-#{n}#{suffix}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/bulldog/util.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Bulldog
|
2
|
+
module Util
|
3
|
+
#
|
4
|
+
# Return the path of the first occurrence of +basename+ in the
|
5
|
+
# current PATH, or nil if the file cannot be found.
|
6
|
+
#
|
7
|
+
def find_in_path(basename)
|
8
|
+
ENV['PATH'].split(/:+/).each do |dirname|
|
9
|
+
path = File.join(dirname, basename)
|
10
|
+
if File.file?(path) && File.executable?(path)
|
11
|
+
return path
|
12
|
+
end
|
13
|
+
end
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Run the given command, logging everything obsessively.
|
19
|
+
#
|
20
|
+
# Return the output if the command is successful, nil otherwise.
|
21
|
+
# If an :expect_status option is given, any status codes in the
|
22
|
+
# list are considered successful.
|
23
|
+
#
|
24
|
+
def run(*command)
|
25
|
+
options = command.last.is_a?(Hash) ? command.pop : {}
|
26
|
+
command.map!{|x| x.to_s}
|
27
|
+
command = Shellwords.join(command) + ' 2>&1'
|
28
|
+
Bulldog.logger.info("[Bulldog] Running: #{command}") if Bulldog.logger
|
29
|
+
output = `#{command}`
|
30
|
+
status = $?.exitstatus
|
31
|
+
if Bulldog.logger
|
32
|
+
Bulldog.logger.info("[Bulldog] Output: #{output}")
|
33
|
+
Bulldog.logger.info("[Bulldog] Status: #{status}")
|
34
|
+
end
|
35
|
+
expected_statuses = options[:expect_status] || [0]
|
36
|
+
expected_statuses.include?(status) ? output : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Backport Shellwords.join from ruby 1.8.7.
|
40
|
+
require 'shellwords'
|
41
|
+
if ::Shellwords.respond_to?(:join)
|
42
|
+
# ruby >= 1.8.7
|
43
|
+
Shellwords = ::Shellwords
|
44
|
+
else
|
45
|
+
module Shellwords
|
46
|
+
#
|
47
|
+
# Escapes a string so that it can be safely used in a Bourne shell
|
48
|
+
# command line.
|
49
|
+
#
|
50
|
+
# Note that a resulted string should be used unquoted and is not
|
51
|
+
# intended for use in double quotes nor in single quotes.
|
52
|
+
#
|
53
|
+
# open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
|
54
|
+
# # ...
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
def escape(str)
|
58
|
+
# An empty argument will be skipped, so return empty quotes.
|
59
|
+
return "''" if str.empty?
|
60
|
+
|
61
|
+
str = str.dup
|
62
|
+
|
63
|
+
# Process as a single byte sequence because not all shell
|
64
|
+
# implementations are multibyte aware.
|
65
|
+
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
|
66
|
+
|
67
|
+
# A LF cannot be escaped with a backslash because a backslash + LF
|
68
|
+
# combo is regarded as line continuation and simply ignored.
|
69
|
+
str.gsub!(/\n/, "'\n'")
|
70
|
+
|
71
|
+
return str
|
72
|
+
end
|
73
|
+
|
74
|
+
module_function :escape
|
75
|
+
|
76
|
+
#
|
77
|
+
# Builds a command line string from an argument list +array+ joining
|
78
|
+
# all elements escaped for Bourne shell and separated by a space.
|
79
|
+
#
|
80
|
+
# open('|' + Shellwords.join(['grep', pattern, *files])) { |pipe|
|
81
|
+
# # ...
|
82
|
+
# }
|
83
|
+
#
|
84
|
+
def join(array)
|
85
|
+
array.map { |arg| escape(arg) }.join(' ')
|
86
|
+
end
|
87
|
+
|
88
|
+
module_function :join
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Bulldog
|
2
|
+
module Validations
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def validates_attachment_presence_of(name, options={})
|
9
|
+
validates_each(name, options) do |record, attribute, attachment|
|
10
|
+
attachment.present? && attachment.file_size > 0 or
|
11
|
+
record.errors.add attribute, options[:message] || :attachment_blank
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def validates_attachment_file_size_of(name, options={})
|
16
|
+
if (range = options.delete(:in))
|
17
|
+
options[:greater_than] = range.min - 1
|
18
|
+
options[:less_than ] = range.max + 1
|
19
|
+
end
|
20
|
+
validates_each(name, options) do |record, attribute, attachment|
|
21
|
+
if attachment.present?
|
22
|
+
file_size = attachment.file_size
|
23
|
+
if options[:greater_than]
|
24
|
+
file_size > options[:greater_than] or
|
25
|
+
record.errors.add attribute, options[:message] || :attachment_too_small
|
26
|
+
end
|
27
|
+
if options[:less_than]
|
28
|
+
file_size < options[:less_than] or
|
29
|
+
record.errors.add attribute, options[:message] || :attachment_too_large
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validates_attachment_type_of(name, options={})
|
36
|
+
validates_each(name, options) do |record, attribute, attachment|
|
37
|
+
if attachment.present?
|
38
|
+
if (pattern = options[:matches])
|
39
|
+
attachment.content_type =~ pattern or
|
40
|
+
record.errors.add attribute, options[:message] || :attachment_wrong_type
|
41
|
+
elsif (specifier = options[:is])
|
42
|
+
if specifier.is_a?(Symbol)
|
43
|
+
attachment_class = Attachment.class_from_type(specifier)
|
44
|
+
attachment.is_a?(attachment_class) or
|
45
|
+
record.errors.add attribute, options[:message] || :attachment_wrong_type
|
46
|
+
else
|
47
|
+
parse_mime_type = lambda do |string|
|
48
|
+
mime_type, parameter_string = string.to_s.split(/;/)
|
49
|
+
parameters = {}
|
50
|
+
(parameter_string || '').split(/,/).each do |pair|
|
51
|
+
name, value = pair.strip.split(/=/)
|
52
|
+
parameters[name] = value
|
53
|
+
end
|
54
|
+
[mime_type, parameters]
|
55
|
+
end
|
56
|
+
|
57
|
+
expected_type, expected_parameters = parse_mime_type.call(specifier)
|
58
|
+
actual_type, actual_parameters = parse_mime_type.call(attachment.content_type)
|
59
|
+
expected_type == actual_type && expected_parameters.all?{|k,v| actual_parameters[k] == v} or
|
60
|
+
record.errors.add attribute, options[:message] || :wrong_type
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Bulldog
|
2
|
+
class Vector2
|
3
|
+
def initialize(object)
|
4
|
+
case object
|
5
|
+
when Array
|
6
|
+
@x, @y = object[0].to_i, object[1].to_i
|
7
|
+
when String
|
8
|
+
match = /([+-]?\d+)[^-+\d]*([+-]?\d+)/.match(object) or
|
9
|
+
raise ArgumentError, "invalid vector: #{object.inspect}"
|
10
|
+
@x, @y = match[1].to_i, match[2].to_i
|
11
|
+
else
|
12
|
+
raise ArgumentError, "cannot convert to vector: #{object.inspect}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :x, :y
|
17
|
+
end
|
18
|
+
end
|
data/rails/init.rb
ADDED
data/script/console
ADDED
data/spec/data/empty.txt
ADDED
File without changes
|
data/spec/data/test.jpg
ADDED
Binary file
|
data/spec/data/test.mov
ADDED
Binary file
|
data/spec/data/test.pdf
ADDED
Binary file
|
data/spec/data/test.png
ADDED
Binary file
|
data/spec/data/test2.jpg
ADDED
Binary file
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module TemporaryDirectory
|
2
|
+
def self.included(mod)
|
3
|
+
mod.before{init_temporary_directory}
|
4
|
+
mod.after{remove_temporary_directory}
|
5
|
+
end
|
6
|
+
|
7
|
+
def temporary_directory
|
8
|
+
"#{ROOT}/spec/tmp"
|
9
|
+
end
|
10
|
+
|
11
|
+
private # ---------------------------------------------------------
|
12
|
+
|
13
|
+
def init_temporary_directory
|
14
|
+
remove_temporary_directory
|
15
|
+
FileUtils.mkdir_p(temporary_directory)
|
16
|
+
|
17
|
+
# When an attachment is deleted, it deletes empty ancestral
|
18
|
+
# directories. Don't delete past the temporary directory.
|
19
|
+
FileUtils.touch "#{temporary_directory}/.do_not_delete"
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_temporary_directory
|
23
|
+
FileUtils.rm_rf(temporary_directory)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module TemporaryModels
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
#
|
7
|
+
# Set up a model class with the given name for all examples in this
|
8
|
+
# example group. +columns+ is a hash from column name to
|
9
|
+
# ActiveRecord type (e.g., :string or :integer).
|
10
|
+
#
|
11
|
+
# The class +name+ may be a String or Symbol. It can also be a
|
12
|
+
# singleton hash mapping the class name to the superclass name. If
|
13
|
+
# the superclass is not ActiveRecord::Base, it is assumed to
|
14
|
+
# indicate single table inheritance, and no table will be created.
|
15
|
+
#
|
16
|
+
def create_model_class(name, columns={}, &class_body)
|
17
|
+
if name.is_a?(Hash) && name.size == 1
|
18
|
+
class_name, superclass_name = *name.to_a.flatten
|
19
|
+
else
|
20
|
+
class_name, superclass_name = name, 'ActiveRecord::Base'
|
21
|
+
end
|
22
|
+
need_table = superclass_name.to_s == 'ActiveRecord::Base'
|
23
|
+
|
24
|
+
table_name = class_name.to_s.underscore.pluralize
|
25
|
+
if need_table
|
26
|
+
ActiveRecord::Base.connection.create_table(table_name) do |table|
|
27
|
+
columns.each do |column_name, column_type|
|
28
|
+
table.send column_type, column_name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
klass = Class.new(superclass_name.to_s.constantize, &class_body)
|
33
|
+
Object.const_set(class_name, klass)
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Destroy a model class created with #create_model_class, and drop
|
38
|
+
# the created table. +name+ should be the same as the first
|
39
|
+
# argument given on creation.
|
40
|
+
#
|
41
|
+
def destroy_model_class(name)
|
42
|
+
if name.is_a?(Hash) && name.size == 1
|
43
|
+
class_name, superclass_name = *name.to_a.flatten
|
44
|
+
else
|
45
|
+
class_name, superclass_name = name, 'ActiveRecord::Base'
|
46
|
+
end
|
47
|
+
need_table = superclass_name.to_s == 'ActiveRecord::Base'
|
48
|
+
|
49
|
+
table_name = class_name.to_s.underscore.pluralize
|
50
|
+
ActiveRecord::Base.connection.drop_table(table_name) if need_table
|
51
|
+
Object.send(:remove_const, class_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Create a model and table for the duration of the block. See
|
56
|
+
# #create_model_class for the meaning of the arguments.
|
57
|
+
#
|
58
|
+
def with_model_class(name, columns={})
|
59
|
+
create_model_class(name, columns)
|
60
|
+
yield
|
61
|
+
ensure
|
62
|
+
destroy_model_class(name)
|
63
|
+
end
|
64
|
+
|
65
|
+
module ClassMethods
|
66
|
+
#
|
67
|
+
# Create a model and table for the duration of each example in
|
68
|
+
# this example group. See #create_model_class for the meaning of
|
69
|
+
# the arguments.
|
70
|
+
#
|
71
|
+
def use_model_class(name, columns={}, &class_body)
|
72
|
+
before{create_model_class(name, columns, &class_body)}
|
73
|
+
after{destroy_model_class(name)}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|