oahu-dragonfly 0.8.2

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.
Files changed (159) hide show
  1. data/.rspec +1 -0
  2. data/.yardopts +24 -0
  3. data/Gemfile +30 -0
  4. data/History.md +323 -0
  5. data/LICENSE +20 -0
  6. data/README.md +88 -0
  7. data/Rakefile +50 -0
  8. data/VERSION +1 -0
  9. data/config.ru +14 -0
  10. data/docs.watchr +1 -0
  11. data/dragonfly.gemspec +297 -0
  12. data/extra_docs/Analysers.md +66 -0
  13. data/extra_docs/Caching.md +23 -0
  14. data/extra_docs/Configuration.md +124 -0
  15. data/extra_docs/Couch.md +49 -0
  16. data/extra_docs/DataStorage.md +153 -0
  17. data/extra_docs/Encoding.md +67 -0
  18. data/extra_docs/GeneralUsage.md +121 -0
  19. data/extra_docs/Generators.md +60 -0
  20. data/extra_docs/Heroku.md +50 -0
  21. data/extra_docs/ImageMagick.md +125 -0
  22. data/extra_docs/Index.md +33 -0
  23. data/extra_docs/MimeTypes.md +40 -0
  24. data/extra_docs/Models.md +272 -0
  25. data/extra_docs/Mongo.md +45 -0
  26. data/extra_docs/Processing.md +77 -0
  27. data/extra_docs/Rack.md +52 -0
  28. data/extra_docs/Rails2.md +57 -0
  29. data/extra_docs/Rails3.md +62 -0
  30. data/extra_docs/Sinatra.md +25 -0
  31. data/extra_docs/URLs.md +169 -0
  32. data/features/images.feature +47 -0
  33. data/features/no_processing.feature +14 -0
  34. data/features/rails_3.0.5.feature +8 -0
  35. data/features/steps/common_steps.rb +8 -0
  36. data/features/steps/dragonfly_steps.rb +66 -0
  37. data/features/steps/rails_steps.rb +28 -0
  38. data/features/support/env.rb +13 -0
  39. data/features/support/setup.rb +32 -0
  40. data/fixtures/rails_3.0.5/files/app/models/album.rb +7 -0
  41. data/fixtures/rails_3.0.5/files/app/views/albums/new.html.erb +7 -0
  42. data/fixtures/rails_3.0.5/files/app/views/albums/show.html.erb +6 -0
  43. data/fixtures/rails_3.0.5/files/config/initializers/dragonfly.rb +4 -0
  44. data/fixtures/rails_3.0.5/files/features/manage_album_images.feature +38 -0
  45. data/fixtures/rails_3.0.5/files/features/step_definitions/helper_steps.rb +7 -0
  46. data/fixtures/rails_3.0.5/files/features/step_definitions/image_steps.rb +25 -0
  47. data/fixtures/rails_3.0.5/files/features/support/paths.rb +17 -0
  48. data/fixtures/rails_3.0.5/files/features/text_images.feature +7 -0
  49. data/fixtures/rails_3.0.5/template.rb +20 -0
  50. data/irbrc.rb +18 -0
  51. data/lib/dragonfly.rb +55 -0
  52. data/lib/dragonfly/active_model_extensions.rb +13 -0
  53. data/lib/dragonfly/active_model_extensions/attachment.rb +250 -0
  54. data/lib/dragonfly/active_model_extensions/attachment_class_methods.rb +148 -0
  55. data/lib/dragonfly/active_model_extensions/class_methods.rb +95 -0
  56. data/lib/dragonfly/active_model_extensions/instance_methods.rb +28 -0
  57. data/lib/dragonfly/active_model_extensions/validations.rb +41 -0
  58. data/lib/dragonfly/analyser.rb +58 -0
  59. data/lib/dragonfly/analysis/file_command_analyser.rb +32 -0
  60. data/lib/dragonfly/analysis/image_magick_analyser.rb +6 -0
  61. data/lib/dragonfly/app.rb +172 -0
  62. data/lib/dragonfly/config/heroku.rb +19 -0
  63. data/lib/dragonfly/config/image_magick.rb +6 -0
  64. data/lib/dragonfly/config/rails.rb +20 -0
  65. data/lib/dragonfly/configurable.rb +207 -0
  66. data/lib/dragonfly/core_ext/array.rb +7 -0
  67. data/lib/dragonfly/core_ext/hash.rb +7 -0
  68. data/lib/dragonfly/core_ext/object.rb +12 -0
  69. data/lib/dragonfly/core_ext/string.rb +9 -0
  70. data/lib/dragonfly/core_ext/symbol.rb +9 -0
  71. data/lib/dragonfly/data_storage.rb +9 -0
  72. data/lib/dragonfly/data_storage/couch_data_store.rb +64 -0
  73. data/lib/dragonfly/data_storage/file_data_store.rb +141 -0
  74. data/lib/dragonfly/data_storage/mongo_data_store.rb +86 -0
  75. data/lib/dragonfly/data_storage/s3data_store.rb +145 -0
  76. data/lib/dragonfly/encoder.rb +13 -0
  77. data/lib/dragonfly/encoding/image_magick_encoder.rb +6 -0
  78. data/lib/dragonfly/function_manager.rb +71 -0
  79. data/lib/dragonfly/generation/image_magick_generator.rb +6 -0
  80. data/lib/dragonfly/generator.rb +9 -0
  81. data/lib/dragonfly/hash_with_css_style_keys.rb +21 -0
  82. data/lib/dragonfly/image_magick/analyser.rb +51 -0
  83. data/lib/dragonfly/image_magick/config.rb +41 -0
  84. data/lib/dragonfly/image_magick/encoder.rb +57 -0
  85. data/lib/dragonfly/image_magick/generator.rb +145 -0
  86. data/lib/dragonfly/image_magick/processor.rb +99 -0
  87. data/lib/dragonfly/image_magick/utils.rb +72 -0
  88. data/lib/dragonfly/image_magick_utils.rb +4 -0
  89. data/lib/dragonfly/job.rb +451 -0
  90. data/lib/dragonfly/job_builder.rb +39 -0
  91. data/lib/dragonfly/job_definitions.rb +26 -0
  92. data/lib/dragonfly/job_endpoint.rb +15 -0
  93. data/lib/dragonfly/loggable.rb +28 -0
  94. data/lib/dragonfly/middleware.rb +20 -0
  95. data/lib/dragonfly/processing/image_magick_processor.rb +6 -0
  96. data/lib/dragonfly/processor.rb +9 -0
  97. data/lib/dragonfly/rails/images.rb +27 -0
  98. data/lib/dragonfly/response.rb +97 -0
  99. data/lib/dragonfly/routed_endpoint.rb +40 -0
  100. data/lib/dragonfly/serializer.rb +32 -0
  101. data/lib/dragonfly/server.rb +113 -0
  102. data/lib/dragonfly/simple_cache.rb +23 -0
  103. data/lib/dragonfly/temp_object.rb +175 -0
  104. data/lib/dragonfly/url_mapper.rb +78 -0
  105. data/samples/beach.png +0 -0
  106. data/samples/egg.png +0 -0
  107. data/samples/round.gif +0 -0
  108. data/samples/sample.docx +0 -0
  109. data/samples/taj.jpg +0 -0
  110. data/spec/dragonfly/active_model_extensions/model_spec.rb +1426 -0
  111. data/spec/dragonfly/active_model_extensions/spec_helper.rb +91 -0
  112. data/spec/dragonfly/analyser_spec.rb +123 -0
  113. data/spec/dragonfly/analysis/file_command_analyser_spec.rb +48 -0
  114. data/spec/dragonfly/app_spec.rb +135 -0
  115. data/spec/dragonfly/configurable_spec.rb +461 -0
  116. data/spec/dragonfly/core_ext/array_spec.rb +19 -0
  117. data/spec/dragonfly/core_ext/hash_spec.rb +19 -0
  118. data/spec/dragonfly/core_ext/string_spec.rb +17 -0
  119. data/spec/dragonfly/core_ext/symbol_spec.rb +17 -0
  120. data/spec/dragonfly/data_storage/couch_data_store_spec.rb +76 -0
  121. data/spec/dragonfly/data_storage/file_data_store_spec.rb +296 -0
  122. data/spec/dragonfly/data_storage/mongo_data_store_spec.rb +57 -0
  123. data/spec/dragonfly/data_storage/s3_data_store_spec.rb +258 -0
  124. data/spec/dragonfly/data_storage/shared_data_store_examples.rb +77 -0
  125. data/spec/dragonfly/function_manager_spec.rb +154 -0
  126. data/spec/dragonfly/hash_with_css_style_keys_spec.rb +24 -0
  127. data/spec/dragonfly/image_magick/analyser_spec.rb +64 -0
  128. data/spec/dragonfly/image_magick/encoder_spec.rb +41 -0
  129. data/spec/dragonfly/image_magick/generator_spec.rb +172 -0
  130. data/spec/dragonfly/image_magick/processor_spec.rb +233 -0
  131. data/spec/dragonfly/image_magick/utils_spec.rb +18 -0
  132. data/spec/dragonfly/job_builder_spec.rb +37 -0
  133. data/spec/dragonfly/job_definitions_spec.rb +35 -0
  134. data/spec/dragonfly/job_endpoint_spec.rb +173 -0
  135. data/spec/dragonfly/job_spec.rb +1046 -0
  136. data/spec/dragonfly/loggable_spec.rb +80 -0
  137. data/spec/dragonfly/middleware_spec.rb +47 -0
  138. data/spec/dragonfly/routed_endpoint_spec.rb +48 -0
  139. data/spec/dragonfly/serializer_spec.rb +61 -0
  140. data/spec/dragonfly/server_spec.rb +278 -0
  141. data/spec/dragonfly/simple_cache_spec.rb +27 -0
  142. data/spec/dragonfly/temp_object_spec.rb +306 -0
  143. data/spec/dragonfly/url_mapper_spec.rb +126 -0
  144. data/spec/functional/deprecations_spec.rb +51 -0
  145. data/spec/functional/image_magick_app_spec.rb +27 -0
  146. data/spec/functional/model_urls_spec.rb +85 -0
  147. data/spec/functional/remote_on_the_fly_spec.rb +51 -0
  148. data/spec/functional/to_response_spec.rb +31 -0
  149. data/spec/spec_helper.rb +51 -0
  150. data/spec/support/argument_matchers.rb +19 -0
  151. data/spec/support/image_matchers.rb +47 -0
  152. data/spec/support/simple_matchers.rb +53 -0
  153. data/yard/handlers/configurable_attr_handler.rb +38 -0
  154. data/yard/setup.rb +15 -0
  155. data/yard/templates/default/fulldoc/html/css/common.css +107 -0
  156. data/yard/templates/default/layout/html/layout.erb +89 -0
  157. data/yard/templates/default/module/html/configuration_summary.erb +31 -0
  158. data/yard/templates/default/module/setup.rb +17 -0
  159. metadata +544 -0
@@ -0,0 +1,28 @@
1
+ module Dragonfly
2
+ module ActiveModelExtensions
3
+ module InstanceMethods
4
+
5
+ def dragonfly_attachments
6
+ @dragonfly_attachments ||= self.class.dragonfly_attachment_classes.inject({}) do |hash, klass|
7
+ hash[klass.attribute] = klass.new(self)
8
+ hash
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def save_dragonfly_attachments
15
+ dragonfly_attachments.each do |attribute, attachment|
16
+ attachment.save!
17
+ end
18
+ end
19
+
20
+ def destroy_dragonfly_attachments
21
+ dragonfly_attachments.each do |attribute, attachment|
22
+ attachment.destroy!
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ module Dragonfly
2
+ module ActiveModelExtensions
3
+ module Validations
4
+
5
+ private
6
+
7
+ def validates_property(property_name, opts)
8
+ attrs = opts[:of] or raise ArgumentError, "you need to provide the attribute which has the property, using :of => <attribute_name>"
9
+ attrs = [attrs].flatten #(make sure it's an array)
10
+
11
+ raise ArgumentError, "you must provide either :in => [<value1>, <value2>..] or :as => <value>" unless opts[:in] || opts[:as]
12
+ allowed_values = opts[:in] || [opts[:as]]
13
+
14
+ args = attrs + [opts]
15
+ validates_each(*args) do |model, attr, attachment|
16
+ if attachment
17
+ property = attachment.send(property_name)
18
+ unless allowed_values.include?(property)
19
+ message = opts[:message] ||
20
+ "#{property_name.to_s.humanize.downcase} is incorrect. "+
21
+ "It needs to be #{Validations.expected_values_string(allowed_values)}"+
22
+ (property ? ", but was '#{property}'" : "")
23
+ message = message.call(property, model) if message.respond_to?(:call)
24
+ model.errors.add(attr, message)
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ def self.expected_values_string(allowed_values)
32
+ if allowed_values.is_a?(Range)
33
+ "between #{allowed_values.first} and #{allowed_values.last}"
34
+ else
35
+ allowed_values.length > 1 ? "one of '#{allowed_values.join('\', \'')}'" : "'#{allowed_values.first.to_s}'"
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,58 @@
1
+ module Dragonfly
2
+ class Analyser < FunctionManager
3
+
4
+ configurable_attr :enable_cache, true
5
+ configurable_attr :cache_size, 100
6
+
7
+ def initialize
8
+ super
9
+ analyser = self
10
+ @analysis_methods = Module.new do
11
+
12
+ define_method :analyser do
13
+ analyser
14
+ end
15
+
16
+ end
17
+ @analysis_method_names = []
18
+ end
19
+
20
+ attr_reader :analysis_methods, :analysis_method_names
21
+
22
+ def analyse(temp_object, method, *args)
23
+ if enable_cache
24
+ key = [temp_object.object_id, method, *args]
25
+ cache[key] ||= call_last(method, temp_object, *args)
26
+ else
27
+ call_last(method, temp_object, *args)
28
+ end
29
+ rescue NotDefined, UnableToHandle => e
30
+ log.warn(e.message)
31
+ nil
32
+ end
33
+
34
+ # Each time a function is registered with the analyser,
35
+ # add a method to the analysis_methods module.
36
+ # Expects the object that is extended to define 'analyse(method, *args)'
37
+ def add(name, *args, &block)
38
+ analysis_methods.module_eval %(
39
+ def #{name}(*args)
40
+ analyse(:#{name}, *args)
41
+ end
42
+ )
43
+ analysis_method_names << name.to_sym
44
+ super
45
+ end
46
+
47
+ def clear_cache!
48
+ @cache = nil
49
+ end
50
+
51
+ private
52
+
53
+ def cache
54
+ @cache ||= SimpleCache.new(cache_size)
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ module Dragonfly
2
+ module Analysis
3
+
4
+ class FileCommandAnalyser
5
+
6
+ include Configurable
7
+
8
+ configurable_attr :file_command, "file"
9
+ configurable_attr :use_filesystem, false
10
+ configurable_attr :num_bytes_to_check, 255
11
+
12
+ def mime_type(temp_object)
13
+ content_type = if use_filesystem
14
+ `#{file_command} -b --mime '#{temp_object.path}'`
15
+ else
16
+ IO.popen("#{file_command} -b --mime -", 'r+') do |io|
17
+ if num_bytes_to_check
18
+ io.write temp_object.data[0, num_bytes_to_check]
19
+ else
20
+ io.write temp_object.data
21
+ end
22
+ io.close_write
23
+ io.read
24
+ end
25
+ end.split(';').first
26
+ content_type.strip if content_type
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ module Dragonfly
2
+ module Analysis
3
+ puts "WARNING: Dragonfly::Analysis::ImageMagickAnalyser is DEPRECATED and will soon be removed. Please use Dragonfly::ImageMagick::Analyser instead."
4
+ ImageMagickAnalyser = ImageMagick::Analyser
5
+ end
6
+ end
@@ -0,0 +1,172 @@
1
+ require 'logger'
2
+ require 'forwardable'
3
+ require 'rack'
4
+
5
+ module Dragonfly
6
+ class App
7
+
8
+ class << self
9
+
10
+ private :new # Hide 'new' - need to use 'instance'
11
+
12
+ def instance(name)
13
+ apps[name] ||= new
14
+ end
15
+
16
+ alias [] instance
17
+
18
+ private
19
+
20
+ def apps
21
+ @apps ||= {}
22
+ end
23
+
24
+ end
25
+
26
+ def initialize
27
+ @analyser, @processor, @encoder, @generator = Analyser.new, Processor.new, Encoder.new, Generator.new
28
+ [@analyser, @processor, @encoder, @generator].each do |obj|
29
+ obj.use_same_log_as(self)
30
+ obj.use_as_fallback_config(self)
31
+ end
32
+ @server = Server.new(self)
33
+ @job_definitions = JobDefinitions.new
34
+ end
35
+
36
+ include Configurable
37
+
38
+ extend Forwardable
39
+ def_delegator :datastore, :destroy
40
+ def_delegators :new_job, :fetch, :generate, :fetch_file, :fetch_url
41
+ def_delegators :server, :call
42
+
43
+ configurable_attr :datastore do DataStorage::FileDataStore.new end
44
+ configurable_attr :cache_duration, 3600*24*365 # (1 year)
45
+ configurable_attr :fallback_mime_type, 'application/octet-stream'
46
+ configurable_attr :secret, 'secret yo'
47
+ configurable_attr :log do Logger.new('/var/tmp/dragonfly.log') end
48
+ configurable_attr :trust_file_extensions, true
49
+ configurable_attr :content_disposition
50
+ configurable_attr :content_filename, Response::DEFAULT_FILENAME
51
+
52
+ attr_reader :analyser
53
+ attr_reader :processor
54
+ attr_reader :encoder
55
+ attr_reader :generator
56
+ attr_reader :server
57
+
58
+ nested_configurable :server, :analyser, :processor, :encoder, :generator
59
+
60
+ attr_accessor :job_definitions
61
+
62
+ def new_job(content=nil, meta={})
63
+ job_class.new(self, content, meta)
64
+ end
65
+ alias create new_job
66
+
67
+ def endpoint(job=nil, &block)
68
+ block ? RoutedEndpoint.new(self, &block) : JobEndpoint.new(job)
69
+ end
70
+
71
+ def job(name, &block)
72
+ job_definitions.add(name, &block)
73
+ end
74
+ configuration_method :job
75
+
76
+ def job_class
77
+ @job_class ||= begin
78
+ app = self
79
+ Class.new(Job).class_eval do
80
+ include app.analyser.analysis_methods
81
+ include app.job_definitions
82
+ include Job::OverrideInstanceMethods
83
+ self
84
+ end
85
+ end
86
+ end
87
+
88
+ def store(object, opts={})
89
+ temp_object = object.is_a?(TempObject) ? object : TempObject.new(object)
90
+ datastore.store(temp_object, opts)
91
+ end
92
+
93
+ def register_mime_type(format, mime_type)
94
+ registered_mime_types[file_ext_string(format)] = mime_type
95
+ end
96
+ configuration_method :register_mime_type
97
+
98
+ def registered_mime_types
99
+ @registered_mime_types ||= Rack::Mime::MIME_TYPES.dup
100
+ end
101
+
102
+ def mime_type_for(format)
103
+ registered_mime_types[file_ext_string(format)]
104
+ end
105
+
106
+ def define_url(&block)
107
+ @url_proc = block
108
+ end
109
+ configuration_method :define_url
110
+
111
+ def url_for(job, opts={})
112
+ if @url_proc
113
+ @url_proc.call(self, job, opts)
114
+ else
115
+ server.url_for(job, opts)
116
+ end
117
+ end
118
+
119
+ def remote_url_for(uid, opts={})
120
+ datastore.url_for(uid, opts)
121
+ rescue NoMethodError => e
122
+ raise NotImplementedError, "The datastore doesn't support serving content directly - #{datastore.inspect}"
123
+ end
124
+
125
+ def define_macro(mod, macro_name)
126
+ already_extended = (class << mod; self; end).included_modules.include?(ActiveModelExtensions)
127
+ mod.extend(ActiveModelExtensions) unless already_extended
128
+ mod.register_dragonfly_app(macro_name, self)
129
+ end
130
+
131
+ def define_macro_on_include(mod, macro_name)
132
+ app = self
133
+ (class << mod; self; end).class_eval do
134
+ alias included_without_dragonfly included
135
+ define_method :included_with_dragonfly do |mod|
136
+ included_without_dragonfly(mod)
137
+ app.define_macro(mod, macro_name)
138
+ end
139
+ alias included included_with_dragonfly
140
+ end
141
+ end
142
+
143
+ # Deprecated methods
144
+ def url_path_prefix=(thing)
145
+ raise NoMethodError, "url_path_prefix is deprecated - please use url_format, e.g. url_format = '/media/:job/:basename.:format' - see docs for more details"
146
+ end
147
+ configuration_method :url_path_prefix=
148
+
149
+ def url_suffix=(thing)
150
+ raise NoMethodError, "url_suffix is deprecated - please use url_format, e.g. url_format = '/media/:job/:basename.:format' - see docs for more details"
151
+ end
152
+ configuration_method :url_suffix=
153
+
154
+ def infer_mime_type_from_file_ext=(bool)
155
+ raise NoMethodError, "infer_mime_type_from_file_ext is deprecated - please use trust_file_extensions = #{bool.inspect} instead"
156
+ end
157
+ configuration_method :infer_mime_type_from_file_ext=
158
+
159
+ private
160
+
161
+ attr_accessor :get_remote_url
162
+
163
+ def saved_configs
164
+ self.class.saved_configs
165
+ end
166
+
167
+ def file_ext_string(format)
168
+ '.' + format.to_s.downcase.sub(/^.*\./,'')
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,19 @@
1
+ module Dragonfly
2
+ module Config
3
+
4
+ module Heroku
5
+
6
+ def self.apply_configuration(app, bucket_name)
7
+ app.configure do |c|
8
+ c.datastore = DataStorage::S3DataStore.new
9
+ c.datastore.configure do |d|
10
+ d.bucket_name = bucket_name
11
+ d.access_key_id = ENV['S3_KEY'] || raise("ENV variable 'S3_KEY' needs to be set - use\n\theroku config:add S3_KEY=XXXXXXXXX")
12
+ d.secret_access_key = ENV['S3_SECRET'] || raise("ENV variable 'S3_SECRET' needs to be set - use\n\theroku config:add S3_SECRET=XXXXXXXXX")
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module Dragonfly
2
+ module Config
3
+ puts "WARNING: Dragonfly::Config::ImageMagick is DEPRECATED and will soon be removed. Please use Dragonfly::ImageMagick::Config instead."
4
+ ImageMagick = Dragonfly::ImageMagick::Config
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ module Dragonfly
2
+ module Config
3
+
4
+ module Rails
5
+
6
+ def self.apply_configuration(app)
7
+ app.configure do |c|
8
+ c.log = ::Rails.logger
9
+ if c.datastore.is_a?(DataStorage::FileDataStore)
10
+ c.datastore.root_path = ::Rails.root.join('public/system/dragonfly', ::Rails.env).to_s
11
+ c.datastore.server_root = ::Rails.root.join('public').to_s
12
+ end
13
+ c.url_format = '/media/:job/:basename.:format'
14
+ c.analyser.register(Analysis::FileCommandAnalyser)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,207 @@
1
+ module Dragonfly
2
+ module Configurable
3
+
4
+ # Exceptions
5
+ class NotConfigured < RuntimeError; end
6
+ class BadConfigAttribute < RuntimeError; end
7
+
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ include Configurable::InstanceMethods
11
+ extend Configurable::ClassMethods
12
+
13
+ # We should use configured_class rather than self.class
14
+ # because sometimes this will be the eigenclass of an object
15
+ # e.g. if we configure a module, etc.
16
+ define_method :configured_class do
17
+ klass
18
+ end
19
+ end
20
+ end
21
+
22
+ class DeferredBlock # Inheriting from Proc causes errors in some versions of Ruby
23
+ def initialize(blk)
24
+ @blk = blk
25
+ end
26
+
27
+ def call
28
+ @blk.call
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ def configure(&block)
35
+ yield ConfigurationProxy.new(self)
36
+ self
37
+ end
38
+
39
+ def configure_with(config, *args, &block)
40
+ config = saved_config_for(config) if config.is_a?(Symbol)
41
+ config.apply_configuration(self, *args)
42
+ configure(&block) if block
43
+ self
44
+ end
45
+
46
+ def has_config_method?(method_name)
47
+ config_methods.include?(method_name.to_sym)
48
+ end
49
+
50
+ def config_methods
51
+ @config_methods ||= configured_class.config_methods.dup
52
+ end
53
+
54
+ def configuration
55
+ @configuration ||= {}
56
+ end
57
+
58
+ def default_configuration
59
+ # Merge the default configuration of all ancestor classes/modules which are configurable
60
+ @default_configuration ||= [self.class, configured_class, *configured_class.ancestors].reverse.inject({}) do |default_config, klass|
61
+ default_config.merge!(klass.default_configuration) if klass.respond_to? :default_configuration
62
+ default_config
63
+ end
64
+ end
65
+
66
+ def set_config_value(key, value)
67
+ configuration[key] = value
68
+ child_configurables.each{|c| c.set_if_unset(key, value) }
69
+ value
70
+ end
71
+
72
+ def use_as_fallback_config(other_configurable)
73
+ other_configurable.add_child_configurable(self)
74
+ self.fallback_configurable = other_configurable
75
+ end
76
+
77
+ protected
78
+
79
+ def add_child_configurable(obj)
80
+ child_configurables << obj
81
+ config_methods.push(*obj.config_methods)
82
+ fallback_configurable.config_methods.push(*obj.config_methods) if fallback_configurable
83
+ end
84
+
85
+ def set_if_unset(key, value)
86
+ set_config_value(key, value) unless set_locally?(key)
87
+ end
88
+
89
+ private
90
+
91
+ attr_accessor :fallback_configurable
92
+
93
+ def child_configurables
94
+ @child_configurables ||= []
95
+ end
96
+
97
+ def set_locally?(key)
98
+ instance_variable_defined?("@#{key}")
99
+ end
100
+
101
+ def default_value(key)
102
+ if default_configuration[key].is_a?(DeferredBlock)
103
+ default_configuration[key] = default_configuration[key].call
104
+ end
105
+ default_configuration[key]
106
+ end
107
+
108
+ def saved_configs
109
+ configured_class.saved_configs
110
+ end
111
+
112
+ def saved_config_for(symbol)
113
+ config = saved_configs[symbol]
114
+ if config.nil?
115
+ raise ArgumentError, "#{symbol.inspect} is not a known configuration - try one of #{saved_configs.keys.join(', ')}"
116
+ end
117
+ config = config.call if config.respond_to?(:call)
118
+ config
119
+ end
120
+
121
+ end
122
+
123
+ module ClassMethods
124
+
125
+ def default_configuration
126
+ @default_configuration ||= {}
127
+ end
128
+
129
+ def config_methods
130
+ @config_methods ||= []
131
+ end
132
+
133
+ def nested_configurables
134
+ @nested_configurables ||= []
135
+ end
136
+
137
+ def register_configuration(name, config=nil, &config_in_block)
138
+ saved_configs[name] = config_in_block || config
139
+ end
140
+
141
+ def saved_configs
142
+ @saved_configs ||= {}
143
+ end
144
+
145
+ private
146
+
147
+ def configurable_attr attribute, default=nil, &blk
148
+ default_configuration[attribute] = blk ? DeferredBlock.new(blk) : default
149
+
150
+ # Define the reader
151
+ define_method(attribute) do
152
+ configuration.has_key?(attribute) ? configuration[attribute] : default_value(attribute)
153
+ end
154
+
155
+ # Define the writer
156
+ define_method("#{attribute}=") do |value|
157
+ instance_variable_set("@#{attribute}", value)
158
+ set_config_value(attribute, value)
159
+ end
160
+
161
+ configuration_method attribute
162
+ configuration_method "#{attribute}="
163
+ end
164
+
165
+ def configuration_method(*method_names)
166
+ config_methods.push(*method_names.map{|n| n.to_sym }).uniq!
167
+ end
168
+
169
+ def nested_configurable(*method_names)
170
+ nested_configurables.push(*method_names)
171
+ end
172
+
173
+ end
174
+
175
+ class ConfigurationProxy
176
+
177
+ def initialize(owner)
178
+ @owner = owner
179
+ end
180
+
181
+ def method_missing(method_name, *args, &block)
182
+ if owner.has_config_method?(method_name)
183
+ attribute = method_name.to_s.tr('=','').to_sym
184
+ if method_name.to_s =~ /=$/ && owner.has_config_method?(attribute) # a bit hacky - if it has both getter and setter, assume it's a configurable_attr
185
+ owner.set_config_value(attribute, args.first)
186
+ else
187
+ owner.send(method_name, *args, &block)
188
+ end
189
+ elsif nested_configurable?(method_name)
190
+ owner.send(method_name)
191
+ else
192
+ raise BadConfigAttribute, "You tried to configure using '#{method_name.inspect}', but the valid config attributes are #{owner.config_methods.map{|a| %('#{a.inspect}') }.sort.join(', ')}"
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ attr_reader :owner
199
+
200
+ def nested_configurable?(method)
201
+ owner.configured_class.nested_configurables.include?(method.to_sym)
202
+ end
203
+
204
+ end
205
+
206
+ end
207
+ end