hanami-view 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,464 @@
1
+ require 'set'
2
+ require 'hanami/utils/class'
3
+ require 'hanami/utils/kernel'
4
+ require 'hanami/utils/string'
5
+ require 'hanami/utils/load_paths'
6
+ require 'hanami/view/rendering/layout_finder'
7
+
8
+ module Hanami
9
+ module View
10
+ # Configuration for the framework, controllers and actions.
11
+ #
12
+ # Hanami::Controller has its own global configuration that can be manipulated
13
+ # via `Hanami::View.configure`.
14
+ #
15
+ # Every time that `Hanami::View` and `Hanami::Layout` are included, that
16
+ # global configuration is being copied to the recipient. The copy will
17
+ # inherit all the settings from the original, but all the subsequent changes
18
+ # aren't reflected from the parent to the children, and viceversa.
19
+ #
20
+ # This architecture allows to have a global configuration that capture the
21
+ # most common cases for an application, and let views and layouts
22
+ # layouts to specify exceptions.
23
+ #
24
+ # @since 0.2.0
25
+ class Configuration
26
+ # Default root
27
+ #
28
+ # @since 0.2.0
29
+ # @api private
30
+ DEFAULT_ROOT = '.'.freeze
31
+
32
+ # Default encoding
33
+ #
34
+ # @since 0.5.0
35
+ # @api private
36
+ DEFAULT_ENCODING = Encoding::UTF_8
37
+
38
+ attr_reader :load_paths
39
+ attr_reader :views
40
+ attr_reader :layouts
41
+ attr_reader :modules
42
+
43
+ # Return the original configuration of the framework instance associated
44
+ # with the given class.
45
+ #
46
+ # When multiple instances of Hanami::View are used in the same application,
47
+ # we want to make sure that a controller or an action will receive the
48
+ # expected configuration.
49
+ #
50
+ # @param base [Class] a view or a layout
51
+ #
52
+ # @return [Hanami::Controller::Configuration] the configuration associated
53
+ # to the given class.
54
+ #
55
+ # @since 0.2.0
56
+ # @api private
57
+ #
58
+ # @example Direct usage of the framework
59
+ # require 'hanami/view'
60
+ #
61
+ # class Show
62
+ # include Hanami::View
63
+ # end
64
+ #
65
+ # Hanami::View::Configuration.for(Show)
66
+ # # => will return from Hanami::View
67
+ #
68
+ # @example Multiple instances of the framework
69
+ # require 'hanami/view'
70
+ #
71
+ # module MyApp
72
+ # View = Hanami::View.duplicate(self)
73
+ #
74
+ # module Views::Dashboard
75
+ # class Index
76
+ # include MyApp::View
77
+ # end
78
+ # end
79
+ # end
80
+ #
81
+ # class Show
82
+ # include Hanami::Action
83
+ # end
84
+ #
85
+ # Hanami::View::Configuration.for(Show)
86
+ # # => will return from Hanami::View
87
+ #
88
+ # Hanami::View::Configuration.for(MyApp::Views::Dashboard::Index)
89
+ # # => will return from MyApp::View
90
+ def self.for(base)
91
+ # TODO this implementation is similar to Hanami::Controller::Configuration consider to extract it into Hanami::Utils
92
+ namespace = Utils::String.new(base).namespace
93
+ framework = Utils::Class.load_from_pattern!("(#{namespace}|Hanami)::View")
94
+ framework.configuration
95
+ end
96
+
97
+ # Initialize a configuration instance
98
+ #
99
+ # @return [Hanami::View::Configuration] a new configuration's instance
100
+ #
101
+ # @since 0.2.0
102
+ def initialize
103
+ @namespace = Object
104
+ reset!
105
+ end
106
+
107
+ # Set the Ruby namespace where to lookup for views.
108
+ #
109
+ # When multiple instances of the framework are used, we want to make sure
110
+ # that if a `MyApp` wants a `Dashboard::Index` view, we are loading the
111
+ # right one.
112
+ #
113
+ # If not set, this value defaults to `Object`.
114
+ #
115
+ # This is part of a DSL, for this reason when this method is called with
116
+ # an argument, it will set the corresponding instance variable. When
117
+ # called without, it will return the already set value, or the default.
118
+ #
119
+ # @overload namespace(value)
120
+ # Sets the given value
121
+ # @param value [Class, Module, String] a valid Ruby namespace identifier
122
+ #
123
+ # @overload namespace
124
+ # Gets the value
125
+ # @return [Class, Module, String]
126
+ #
127
+ # @since 0.2.0
128
+ #
129
+ # @example Getting the value
130
+ # require 'hanami/view'
131
+ #
132
+ # Hanami::View.configuration.namespace # => Object
133
+ #
134
+ # @example Setting the value
135
+ # require 'hanami/view'
136
+ #
137
+ # Hanami::View.configure do
138
+ # namespace 'MyApp::Views'
139
+ # end
140
+ def namespace(value = nil)
141
+ if value
142
+ @namespace = value
143
+ else
144
+ @namespace
145
+ end
146
+ end
147
+
148
+ # Set the root path where to search for templates
149
+ #
150
+ # If not set, this value defaults to the current directory.
151
+ #
152
+ # This is part of a DSL, for this reason when this method is called with
153
+ # an argument, it will set the corresponding instance variable. When
154
+ # called without, it will return the already set value, or the default.
155
+ #
156
+ # @overload root(value)
157
+ # Sets the given value
158
+ # @param value [String,Pathname,#to_pathname] an object that can be
159
+ # coerced to Pathname
160
+ # @raise [Errno::ENOENT] if the given path doesn't exist
161
+ #
162
+ # @overload root
163
+ # Gets the value
164
+ # @return [Pathname]
165
+ #
166
+ # @since 0.2.0
167
+ #
168
+ # @see Hanami::View::Dsl#root
169
+ # @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/pathname/rdoc/Pathname.html
170
+ # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Pathname-class_method
171
+ #
172
+ # @example Getting the value
173
+ # require 'hanami/view'
174
+ #
175
+ # Hanami::View.configuration.root # => #<Pathname:.>
176
+ #
177
+ # @example Setting the value
178
+ # require 'hanami/view'
179
+ #
180
+ # Hanami::View.configure do
181
+ # root '/path/to/templates'
182
+ # end
183
+ #
184
+ # Hanami::View.configuration.root # => #<Pathname:/path/to/templates>
185
+ def root(value = nil)
186
+ if value
187
+ @root = Utils::Kernel.Pathname(value).realpath
188
+ else
189
+ @root
190
+ end
191
+ end
192
+
193
+ # Set the global layout
194
+ #
195
+ # If not set, this value defaults to `nil`, while at the rendering time
196
+ # it will use `Hanami::View::Rendering::NullLayout`.
197
+ #
198
+ # This is part of a DSL, for this reason when this method is called with
199
+ # an argument, it will set the corresponding instance variable. When
200
+ # called without, it will return the already set value, or the default.
201
+ #
202
+ # @overload layout(value)
203
+ # Sets the given value
204
+ # @param value [Symbol] the name of the layout
205
+ #
206
+ # @overload layout
207
+ # Gets the value
208
+ # @return [Class]
209
+ #
210
+ # @since 0.2.0
211
+ #
212
+ # @see Hanami::View::Dsl#layout
213
+ #
214
+ # @example Getting the value
215
+ # require 'hanami/view'
216
+ #
217
+ # Hanami::View.configuration.layout # => nil
218
+ #
219
+ # @example Setting the value
220
+ # require 'hanami/view'
221
+ #
222
+ # Hanami::View.configure do
223
+ # layout :application
224
+ # end
225
+ #
226
+ # Hanami::View.configuration.layout # => ApplicationLayout
227
+ #
228
+ # @example Setting the value in a namespaced app
229
+ # require 'hanami/view'
230
+ #
231
+ # module MyApp
232
+ # View = Hanami::View.duplicate(self) do
233
+ # layout :application
234
+ # end
235
+ # end
236
+ #
237
+ # MyApp::View.configuration.layout # => MyApp::ApplicationLayout
238
+ def layout(value = nil)
239
+ if value.nil?
240
+ Rendering::LayoutFinder.find(@layout, @namespace)
241
+ else
242
+ @layout = value
243
+ end
244
+ end
245
+
246
+ # Default encoding for templates
247
+ #
248
+ # This is part of a DSL, for this reason when this method is called with
249
+ # an argument, it will set the corresponding instance variable. When
250
+ # called without, it will return the already set value, or the default.
251
+ #
252
+ # @overload default_encoding(value)
253
+ # Sets the given value
254
+ # @param value [String,Encoding] a string representation of the encoding,
255
+ # or an Encoding constant
256
+ #
257
+ # @raise [ArgumentError] if the given value isn't a supported encoding
258
+ #
259
+ # @overload default_encoding
260
+ # Gets the value
261
+ # @return [Encoding]
262
+ #
263
+ # @since 0.5.0
264
+ #
265
+ # @example Set UTF-8 As A String
266
+ # require 'hanami/view'
267
+ #
268
+ # Hanami::View.configure do
269
+ # default_encoding 'utf-8'
270
+ # end
271
+ #
272
+ # @example Set UTF-8 As An Encoding Constant
273
+ # require 'hanami/view'
274
+ #
275
+ # Hanami::View.configure do
276
+ # default_encoding Encoding::UTF_8
277
+ # end
278
+ #
279
+ # @example Raise An Error For Unknown Encoding
280
+ # require 'hanami/view'
281
+ #
282
+ # Hanami::View.configure do
283
+ # default_encoding 'foo'
284
+ # end
285
+ #
286
+ # # => ArgumentError
287
+ def default_encoding(value = nil)
288
+ if value.nil?
289
+ @default_encoding
290
+ else
291
+ @default_encoding = Encoding.find(value)
292
+ end
293
+ end
294
+
295
+ # Prepare the views.
296
+ #
297
+ # The given block will be yielded when `Hanami::View` will be included by
298
+ # a view.
299
+ #
300
+ # This method can be called multiple times.
301
+ #
302
+ # @param blk [Proc] the code block
303
+ #
304
+ # @return [void]
305
+ #
306
+ # @raise [ArgumentError] if called without passing a block
307
+ #
308
+ # @since 0.3.0
309
+ #
310
+ # @see Hanami::View.configure
311
+ # @see Hanami::View.duplicate
312
+ #
313
+ # @example Including shared utilities
314
+ # require 'hanami/view'
315
+ #
316
+ # module UrlHelpers
317
+ # def comments_path
318
+ # '/'
319
+ # end
320
+ # end
321
+ #
322
+ # Hanami::View.configure do
323
+ # prepare do
324
+ # include UrlHelpers
325
+ # end
326
+ # end
327
+ #
328
+ # Hanami::View.load!
329
+ #
330
+ # module Comments
331
+ # class New
332
+ # # The following include will cause UrlHelpers to be included too.
333
+ # # This makes `comments_path` available in the view context
334
+ # include Hanami::View
335
+ #
336
+ # def form
337
+ # %(<form action="#{ comments_path }" method="POST"></form>)
338
+ # end
339
+ # end
340
+ # end
341
+ #
342
+ # @example Preparing multiple times
343
+ # require 'hanami/view'
344
+ #
345
+ # Hanami::View.configure do
346
+ # prepare do
347
+ # include UrlHelpers
348
+ # end
349
+ #
350
+ # prepare do
351
+ # format :json
352
+ # end
353
+ # end
354
+ #
355
+ # Hanami::View.configure do
356
+ # prepare do
357
+ # include FormattingHelpers
358
+ # end
359
+ # end
360
+ #
361
+ # Hanami::View.load!
362
+ #
363
+ # module Articles
364
+ # class Index
365
+ # # The following include will cause the inclusion of:
366
+ # # * UrlHelpers
367
+ # # * FormattingHelpers
368
+ # #
369
+ # # It also sets the view to render only JSON
370
+ # include Hanami::View
371
+ # end
372
+ # end
373
+ def prepare(&blk)
374
+ if block_given?
375
+ @modules.push(blk)
376
+ else
377
+ raise ArgumentError.new('Please provide a block')
378
+ end
379
+ end
380
+
381
+ # Add a view to the registry
382
+ #
383
+ # @since 0.2.0
384
+ # @api private
385
+ def add_view(view)
386
+ @views.add(view)
387
+ end
388
+
389
+ # Add a layout to the registry
390
+ #
391
+ # @since 0.2.0
392
+ # @api private
393
+ def add_layout(layout)
394
+ @layouts.add(layout)
395
+ end
396
+
397
+ # Duplicate by copying the settings in a new instance.
398
+ #
399
+ # @return [Hanami::View::Configuration] a copy of the configuration
400
+ #
401
+ # @since 0.2.0
402
+ # @api private
403
+ def duplicate
404
+ Configuration.new.tap do |c|
405
+ c.namespace = namespace
406
+ c.root = root
407
+ c.layout = @layout # lazy loading of the class
408
+ c.default_encoding = default_encoding
409
+ c.load_paths = load_paths.dup
410
+ c.modules = modules.dup
411
+ end
412
+ end
413
+
414
+ # Load the configuration for the current framework
415
+ #
416
+ # @since 0.2.0
417
+ # @api private
418
+ def load!
419
+ views.each { |v| v.__send__(:load!) }
420
+ layouts.each { |l| l.__send__(:load!) }
421
+ freeze
422
+ end
423
+
424
+ # Reset all the values to the defaults
425
+ #
426
+ # @since 0.2.0
427
+ # @api private
428
+ def reset!
429
+ root DEFAULT_ROOT
430
+ default_encoding DEFAULT_ENCODING
431
+
432
+ @views = Set.new
433
+ @layouts = Set.new
434
+ @load_paths = Utils::LoadPaths.new(root)
435
+ @layout = nil
436
+ @modules = []
437
+ end
438
+
439
+ # Copy the configuration for the given action
440
+ #
441
+ # @param base [Class] the target action
442
+ #
443
+ # @return void
444
+ #
445
+ # @since 0.3.0
446
+ # @api private
447
+ def copy!(base)
448
+ modules.each do |mod|
449
+ base.class_eval(&mod)
450
+ end
451
+ end
452
+
453
+ alias_method :unload!, :reset!
454
+
455
+ protected
456
+ attr_writer :namespace
457
+ attr_writer :root
458
+ attr_writer :load_paths
459
+ attr_writer :layout
460
+ attr_writer :default_encoding
461
+ attr_writer :modules
462
+ end
463
+ end
464
+ end