bulldog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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