adrift 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +10 -0
- data/Rakefile +22 -0
- data/adrift.gemspec +38 -0
- data/autotest/discover.rb +1 -0
- data/features/active_record_integration.feature +42 -0
- data/features/data_mapper_integration.feature +42 -0
- data/features/step_definitions/model_steps.rb +54 -0
- data/features/support/env.rb +55 -0
- data/features/support/world.rb +58 -0
- data/lib/adrift.rb +10 -0
- data/lib/adrift/attachment.rb +246 -0
- data/lib/adrift/file_to_attach.rb +121 -0
- data/lib/adrift/integration.rb +39 -0
- data/lib/adrift/integration/active_record.rb +29 -0
- data/lib/adrift/integration/base.rb +87 -0
- data/lib/adrift/integration/data_mapper.rb +29 -0
- data/lib/adrift/pattern.rb +219 -0
- data/lib/adrift/processor.rb +100 -0
- data/lib/adrift/railtie.rb +12 -0
- data/lib/adrift/storage.rb +82 -0
- data/lib/adrift/version.rb +3 -0
- data/spec/adrift/attachment_spec.rb +488 -0
- data/spec/adrift/file_to_attach_spec.rb +78 -0
- data/spec/adrift/integration/active_record_spec.rb +21 -0
- data/spec/adrift/integration/base_spec.rb +7 -0
- data/spec/adrift/integration/data_mapper_spec.rb +21 -0
- data/spec/adrift/pattern_spec.rb +98 -0
- data/spec/adrift/processor_spec.rb +61 -0
- data/spec/adrift/storage_spec.rb +181 -0
- data/spec/fixtures/me.png +0 -0
- data/spec/fixtures/me_no_colors.png +0 -0
- data/spec/shared_examples/integration/base.rb +128 -0
- data/spec/spec_helper.rb +47 -0
- metadata +277 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
module Adrift
|
2
|
+
class UnknownFileRepresentationError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
# Factory of the objects that Attachment#assign expects. Theese are
|
6
|
+
# adapters for the Rack and Rails' uploaded file representations and
|
7
|
+
# File instances.
|
8
|
+
module FileToAttach
|
9
|
+
# Common adapter behaviour for the files who will be attached.
|
10
|
+
module Adapter
|
11
|
+
# Creates a new Adapter for the +file_representation+.
|
12
|
+
def initialize(file_representation)
|
13
|
+
@file_representation = file_representation
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Namespace containing the adapters for the objects that
|
18
|
+
# Attachment#assign expects. They need to respond to
|
19
|
+
# :original_filename and :path (what theese methods return it's
|
20
|
+
# pretty much self-explanatory).
|
21
|
+
module Adapters
|
22
|
+
# Adapter that allows to attach an uploaded file within a Rack
|
23
|
+
# (non-Rails) application.
|
24
|
+
class Rack
|
25
|
+
include Adapter
|
26
|
+
|
27
|
+
# Indicates whether or not +file_representation+ is an
|
28
|
+
# uploaded file within a Rack application.
|
29
|
+
def self.recognize?(file_representation)
|
30
|
+
file_representation.respond_to?(:has_key?) &&
|
31
|
+
file_representation.has_key?(:filename) &&
|
32
|
+
file_representation.has_key?(:tempfile)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Uploaded file's original filename.
|
36
|
+
def original_filename
|
37
|
+
@file_representation[:filename]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Uploaded file's path.
|
41
|
+
def path
|
42
|
+
@file_representation[:tempfile].path
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Adapter that allows to attach an uploaded file within a Rails
|
47
|
+
# application.
|
48
|
+
class Rails
|
49
|
+
include Adapter
|
50
|
+
|
51
|
+
# Indicates whether or not +file_representation+ is an
|
52
|
+
# uploaded file within a Rails application.
|
53
|
+
def self.recognize?(file_representation)
|
54
|
+
file_representation.respond_to?(:original_filename) &&
|
55
|
+
file_representation.respond_to?(:tempfile)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Uploaded file's original filename.
|
59
|
+
def original_filename
|
60
|
+
@file_representation.original_filename
|
61
|
+
end
|
62
|
+
|
63
|
+
# Uploaded file's path.
|
64
|
+
def path
|
65
|
+
@file_representation.tempfile.path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Adapter that allow to attach a local file.
|
70
|
+
class LocalFile
|
71
|
+
include Adapter
|
72
|
+
|
73
|
+
# Indicates whether or not +file_representation+ is a local
|
74
|
+
# file.
|
75
|
+
def self.recognize?(file_representation)
|
76
|
+
file_representation.respond_to?(:to_path)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Local file's name.
|
80
|
+
def original_filename
|
81
|
+
::File.basename(@file_representation.to_path)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Local file's path.
|
85
|
+
def path
|
86
|
+
@file_representation.to_path
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Creates a new object that will that will act as an adapter for
|
92
|
+
# +file_representation+, the object that represents the file to be
|
93
|
+
# attached. This adapter wil have the interface expected by
|
94
|
+
# Attachment#assign.
|
95
|
+
#
|
96
|
+
# Raises Adrift::UnknownFileRepresentationError when it can't
|
97
|
+
# recognize +file_representation+.
|
98
|
+
def self.new(file_representation)
|
99
|
+
adapter_class = find_adapter_class(file_representation)
|
100
|
+
raise UnknownFileRepresentationError if adapter_class.nil?
|
101
|
+
adapter_class.new(file_representation)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Finds the class of the object who will act as an adapter for
|
107
|
+
# +file_representation+.
|
108
|
+
def self.find_adapter_class(file_representation)
|
109
|
+
adapter_classes.find do |adapter|
|
110
|
+
adapter.respond_to?(:recognize?) &&
|
111
|
+
adapter.recognize?(file_representation)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Lists the classes of the objects that act as adapters for the
|
116
|
+
# file representations.
|
117
|
+
def self.adapter_classes
|
118
|
+
Adapters.constants.map { |class_name| Adapters.const_get(class_name) }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'adrift/integration/active_record'
|
2
|
+
require 'adrift/integration/data_mapper'
|
3
|
+
|
4
|
+
module Adrift
|
5
|
+
# Namespace for the modules that ease the usage of Adrift in
|
6
|
+
# conjuction with ORM libraries.
|
7
|
+
#
|
8
|
+
# This won't be loaded by default. In order to automatically try to
|
9
|
+
# integrate Adrift with the ORM library in use (just ActiveRecord or
|
10
|
+
# DataMapper for now), it is necessary to require this feature
|
11
|
+
# manually:
|
12
|
+
#
|
13
|
+
# require 'adrift/integration'
|
14
|
+
#
|
15
|
+
# It is also possible to request Adrift to integrate with a specific
|
16
|
+
# library by requiring the corresponding integration feature:
|
17
|
+
#
|
18
|
+
# require 'adrift/integration/active_record'
|
19
|
+
# require 'adrift/integration/data_mapper'
|
20
|
+
#
|
21
|
+
# In order to any of this to work, the ORM library must be loaded
|
22
|
+
# before Adrift. However, if (for whatever reason) that isn't the
|
23
|
+
# case, besides requiring the integration for the library, it must
|
24
|
+
# be installed by calling +install+ on the appropiate module. For
|
25
|
+
# instance, to integrate Adrift with ActiveRecord:
|
26
|
+
#
|
27
|
+
# require 'adrift/integration/active_record'
|
28
|
+
# Adrift::Integration::ActiveRecord.install
|
29
|
+
#
|
30
|
+
# When the integration is ready, Base#attachment will become
|
31
|
+
# available as a method of the model classes. For instance, when
|
32
|
+
# using ActiveRecord:
|
33
|
+
#
|
34
|
+
# class User < ActiveRecord::Base
|
35
|
+
# attachment :avatar
|
36
|
+
# end
|
37
|
+
module Integration
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'adrift/integration/base'
|
2
|
+
|
3
|
+
module Adrift
|
4
|
+
module Integration
|
5
|
+
# Integrates Adrift with ActiveRecord.
|
6
|
+
module ActiveRecord
|
7
|
+
include Base
|
8
|
+
|
9
|
+
# Does everything Base#attachment does, but it also registers
|
10
|
+
# the callbacks to save the attachments when the model is saved,
|
11
|
+
# and to destroy them, when it is destroyed.
|
12
|
+
def attachment(*)
|
13
|
+
super
|
14
|
+
after_save :save_attachments
|
15
|
+
before_destroy :destroy_attachments
|
16
|
+
end
|
17
|
+
|
18
|
+
# Integrates Adrift with ActiveRecord if it has been loaded.
|
19
|
+
def self.install
|
20
|
+
if defined?(::ActiveRecord::Base)
|
21
|
+
::ActiveRecord::Base.send(:extend, self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
install
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Adrift
|
2
|
+
module Integration
|
3
|
+
# Common integration code.
|
4
|
+
module Base
|
5
|
+
# Methods that handle the communication with the Attachments of
|
6
|
+
# a given model.
|
7
|
+
#
|
8
|
+
# They are included in the model class by Base#attachment.
|
9
|
+
module InstanceMethods
|
10
|
+
# Attachment objects that belongs to the model. It needs the
|
11
|
+
# model class to be able to respond to
|
12
|
+
# +attachment_definitions+ with a Hash where the keys are the
|
13
|
+
# Attachment names.
|
14
|
+
def attachments
|
15
|
+
self.class.attachment_definitions.keys.map { |name| send(name) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sends +message+ to the Attachment objects that belongs to
|
19
|
+
# the model.
|
20
|
+
def send_to_attachments(message)
|
21
|
+
attachments.each { |attachment| attachment.send(message) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Sends the message :save to the Attachment objects that
|
25
|
+
# belongs to the model.
|
26
|
+
def save_attachments
|
27
|
+
send_to_attachments(:save)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sends the message :destroy to the Attachment objects that
|
31
|
+
# belongs to the model.
|
32
|
+
def destroy_attachments
|
33
|
+
send_to_attachments(:destroy)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Defines accessor methods for the Attachment, includes
|
38
|
+
# InstanceMethods in the model class, and stores the attachment
|
39
|
+
# +name+ and +options+ for future reference (see
|
40
|
+
# #attachment_definitions).
|
41
|
+
#
|
42
|
+
# +name+ and +options+ are the arguments that Attachment::new
|
43
|
+
# expects, and receives, with the exception that it accepts an
|
44
|
+
# <tt>:class</tt> option with a custom class to use instead of
|
45
|
+
# Attachment. In that case, <tt>options[:class]</tt> will
|
46
|
+
# receive +new+ with +name+, the model, and +options+ without
|
47
|
+
# +:class+.
|
48
|
+
#
|
49
|
+
# The accessor methods are named after the Attachment. For
|
50
|
+
# instance, the following code will define +avatar+ and
|
51
|
+
# <tt>avatar=</tt> on the model class that receives #attachment:
|
52
|
+
#
|
53
|
+
# attachment :avatar
|
54
|
+
#
|
55
|
+
# The writter method (in the example: <tt>avatar=</tt>) will
|
56
|
+
# assign to the Attachment the results of calling
|
57
|
+
# FileToAttach::new with its argument. See Attachment#assign
|
58
|
+
# for more details.
|
59
|
+
def attachment(name, options={})
|
60
|
+
include InstanceMethods
|
61
|
+
|
62
|
+
attachment_definitions[name] = options.dup
|
63
|
+
attachment_class = options.delete(:class) || Attachment
|
64
|
+
|
65
|
+
define_method(name) do
|
66
|
+
instance_variable = "@#{name}_attachment"
|
67
|
+
unless instance_variable_defined?(instance_variable)
|
68
|
+
attachment = attachment_class.new(name, self, options)
|
69
|
+
instance_variable_set(instance_variable, attachment)
|
70
|
+
end
|
71
|
+
instance_variable_get(instance_variable)
|
72
|
+
end
|
73
|
+
|
74
|
+
define_method("#{name}=") do |file_representation|
|
75
|
+
send(name).assign(FileToAttach.new(file_representation))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Attachment definitions for the model. Is a Hash where the
|
80
|
+
# keys are the +names+ and the values are the +options+ passed
|
81
|
+
# to #attachment.
|
82
|
+
def attachment_definitions
|
83
|
+
@attachment_definitions ||= {}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'adrift/integration/base'
|
2
|
+
|
3
|
+
module Adrift
|
4
|
+
module Integration
|
5
|
+
# Integrates Adrift with DataMapper.
|
6
|
+
module DataMapper
|
7
|
+
include Base
|
8
|
+
|
9
|
+
# Does everything Base#attachment does, but it also registers
|
10
|
+
# the callbacks to save the attachments when the model is saved,
|
11
|
+
# and to destroy them, when it is destroyed.
|
12
|
+
def attachment(*)
|
13
|
+
super
|
14
|
+
after :save, :save_attachments
|
15
|
+
before :destroy, :destroy_attachments
|
16
|
+
end
|
17
|
+
|
18
|
+
# Integrates Adrift with DataMapper if it has been loaded.
|
19
|
+
def self.install
|
20
|
+
if defined?(::DataMapper::Model)
|
21
|
+
::DataMapper::Model.append_extensions(self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
install
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module Adrift
|
4
|
+
# Provides a way for Attachment to generally define its paths and
|
5
|
+
# urls, allowing to specialize them for every individual instance.
|
6
|
+
#
|
7
|
+
# In order to do this, a Pattern is build from a String comprised of
|
8
|
+
# Tags (or more precisely, their labels) and when is asked to be
|
9
|
+
# specialized for a given Attachment and style, it replaces these
|
10
|
+
# Tags with they specialized values for that Attachment and that
|
11
|
+
# style.
|
12
|
+
class Pattern
|
13
|
+
# Namespace containing the Tag objects used by Pattern.
|
14
|
+
#
|
15
|
+
# They are the building blocks of a Pattern. A Pattern is
|
16
|
+
# specialized by specializing the Tags that appear in its
|
17
|
+
# string. They need to satisfy the following interface:
|
18
|
+
#
|
19
|
+
# [+#label+]
|
20
|
+
# Portion of Pattern#string that is replaced with the returned
|
21
|
+
# value of +#specialize+.
|
22
|
+
#
|
23
|
+
# [<tt>#specialize(options)</tt>]
|
24
|
+
# Value that will replace the label in the Pattern (+options+
|
25
|
+
# are the same passed to Pattern#specialize).
|
26
|
+
module Tags
|
27
|
+
# Pattern's tag that allows to generally express the
|
28
|
+
# Attachment's name.
|
29
|
+
class Attachment
|
30
|
+
# Portion of Pattern#string that will be replaced.
|
31
|
+
def label
|
32
|
+
':attachment'
|
33
|
+
end
|
34
|
+
|
35
|
+
# Pluralized Attachment's name. Expects +options+ to include
|
36
|
+
# the Attachment (+:attachment+ key).
|
37
|
+
def specialize(options={})
|
38
|
+
options[:attachment].name.to_s.underscore.pluralize
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Pattern's tag that allows to generally express the selected
|
43
|
+
# style.
|
44
|
+
class Style
|
45
|
+
# Portion of Pattern#string that will be replaced.
|
46
|
+
def label
|
47
|
+
':style'
|
48
|
+
end
|
49
|
+
|
50
|
+
# Selected style, expects +options+ to include it (+:style+
|
51
|
+
# key).
|
52
|
+
def specialize(options={})
|
53
|
+
options[:style].to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Pattern's tag that allows to generally express the
|
58
|
+
# Attachment's url.
|
59
|
+
class Url
|
60
|
+
# Portion of Pattern#string that will be replaced.
|
61
|
+
def label
|
62
|
+
':url'
|
63
|
+
end
|
64
|
+
|
65
|
+
# Attachment's url. Expects +options+ to include the
|
66
|
+
# Attachment (+:attachment+ key), and the selected style
|
67
|
+
# (+:style+ key). .
|
68
|
+
def specialize(options={})
|
69
|
+
options[:attachment].url(options[:style]).to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Pattern's tag that allows to generally express the model's to
|
74
|
+
# which the Attachment belongs class name including its namespace.
|
75
|
+
class Class
|
76
|
+
# Portion of Pattern#string that will be replaced.
|
77
|
+
def label
|
78
|
+
':class'
|
79
|
+
end
|
80
|
+
|
81
|
+
# Pluralized model's class name namespaced. Expects +options+
|
82
|
+
# to include the Attachment (+:attachment+ key).
|
83
|
+
def specialize(options={})
|
84
|
+
options[:attachment].model.class.name.underscore.pluralize
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Pattern's tag that allows to generally express the model's to
|
89
|
+
# which the Attachment belongs class name, without its
|
90
|
+
# namespace.
|
91
|
+
class ClassName
|
92
|
+
# Portion of Pattern#string that will be replaced.
|
93
|
+
def label
|
94
|
+
':class_name'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Pluralized model's class name no namespaced. Expects
|
98
|
+
# +options+ to include the Attachment (+:attachment+ key).
|
99
|
+
def specialize(options={})
|
100
|
+
options[:attachment].model.class.name.demodulize.underscore.pluralize
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Pattern's tag that allows to generally express the model's to
|
105
|
+
# which the Attachment belongs ID.
|
106
|
+
class Id
|
107
|
+
# Portion of Pattern#string that will be replaced.
|
108
|
+
def label
|
109
|
+
':id'
|
110
|
+
end
|
111
|
+
|
112
|
+
# Model's ID. Expects +options+ to include the Attachment
|
113
|
+
# (+:attachment+ key).
|
114
|
+
def specialize(options={})
|
115
|
+
options[:attachment].model.id.to_s
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Pattern's tag that represents the application root directory.
|
120
|
+
class Root
|
121
|
+
class << self
|
122
|
+
attr_accessor :path
|
123
|
+
end
|
124
|
+
|
125
|
+
# Portion of Pattern#string that will be replaced.
|
126
|
+
def label
|
127
|
+
':root'
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns Adrift::Pattern::Tags::Root.path when defined, '.'
|
131
|
+
# otherwise.
|
132
|
+
def specialize(*)
|
133
|
+
self.class.path || '.'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Pattern's tag that allows to generally express the
|
138
|
+
# Attachment's file name.
|
139
|
+
class Filename
|
140
|
+
# Portion of Pattern#string that will be replaced.
|
141
|
+
def label
|
142
|
+
':filename'
|
143
|
+
end
|
144
|
+
|
145
|
+
# Attachment's filename. Expects +options+ to include the
|
146
|
+
# Attachment (+:attachment+ key).
|
147
|
+
def specialize(options={})
|
148
|
+
options[:attachment].filename.to_s
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Pattern's tag that allows to generally express the
|
153
|
+
# Attachment's file base name.
|
154
|
+
class Basename
|
155
|
+
# Portion of Pattern#string that will be replaced.
|
156
|
+
def label
|
157
|
+
':basename'
|
158
|
+
end
|
159
|
+
|
160
|
+
# Attachment's file base name. Expects +options+ to include
|
161
|
+
# the Attachment (+:attachment+ key).
|
162
|
+
def specialize(options={})
|
163
|
+
filename = options[:attachment].filename.to_s
|
164
|
+
filename.sub(File.extname(filename), '')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Pattern's tag that allows to generally express the
|
169
|
+
# Attachment's extension.
|
170
|
+
class Extension
|
171
|
+
# Portion of Pattern#string that will be replaced.
|
172
|
+
def label
|
173
|
+
':extension'
|
174
|
+
end
|
175
|
+
|
176
|
+
# Attachment's file extension. Expects +options+ to include
|
177
|
+
# the Attachment (+:attachment+ key).
|
178
|
+
def specialize(options={})
|
179
|
+
File.extname(options[:attachment].filename.to_s).sub('.', '')
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Tags every instance of Pattern will be able to recognize (and
|
185
|
+
# specialize).
|
186
|
+
def self.tags
|
187
|
+
@tags ||= []
|
188
|
+
end
|
189
|
+
|
190
|
+
attr_reader :string
|
191
|
+
|
192
|
+
# Creates a new Pattern from a +string+ comprised of one o more
|
193
|
+
# tag's labels.
|
194
|
+
def initialize(string)
|
195
|
+
@string = string.dup
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns #string with the known Tags replaced with their specific
|
199
|
+
# values the given +options+. While +options+ is just a Hash,
|
200
|
+
# it's expected to include the Attachment this Pattern belongs to
|
201
|
+
# (+:attachment+ key), and the selected style (+:style+ key).
|
202
|
+
def specialize(options={})
|
203
|
+
sorted_tags.inject(string) do |result, tag|
|
204
|
+
result.gsub(tag.label) { tag.specialize(options) }
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
# Known Tags sorted in reverse label order.
|
211
|
+
def sorted_tags
|
212
|
+
self.class.tags.sort_by(&:label).reverse
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
Pattern::Tags.constants.each do |class_name|
|
217
|
+
Pattern.tags << Pattern::Tags.const_get(class_name).new
|
218
|
+
end
|
219
|
+
end
|