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.
@@ -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