adrift 0.0.1

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