hanami-view 0.0.0 → 0.6.0

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,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