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,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
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'bulldog'
2
+
3
+ Bulldog.instance_eval do
4
+ self.logger = Rails.logger
5
+
6
+ to_interpolate(:rails_root){Rails.root}
7
+ to_interpolate(:rails_env){Rails.env}
8
+ to_interpolate(:public_path){Rails.public_path}
9
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'irb'
4
+ $:.unshift 'lib'
5
+ require 'active_record'
6
+ require 'bulldog'
7
+ require 'init'
8
+ IRB.start
File without changes
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ module ImageCreation
2
+ def create_image(path, options={})
3
+ options[:size] ||= '10x10'
4
+ convert = Bulldog::Processor::ImageMagick.convert_command
5
+ system(convert, '-geometry', "#{options[:size]}!", 'pattern:checkerboard', path)
6
+ path
7
+ end
8
+ end
@@ -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