adrift 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 +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
|