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,54 @@
1
+ module Hanami
2
+ module View
3
+ # Inheriting mechanisms
4
+ #
5
+ # @since 0.1.0
6
+ module Inheritable
7
+ # Register a view subclass
8
+ #
9
+ # @api private
10
+ # @since 0.1.0
11
+ #
12
+ # @example
13
+ # require 'hanami/view'
14
+ #
15
+ # class IndexView
16
+ # include Hanami::View
17
+ # end
18
+ #
19
+ # class JsonIndexView < IndexView
20
+ # end
21
+ def inherited(base)
22
+ subclasses.add base
23
+ end
24
+
25
+ # Set of registered subclasses
26
+ #
27
+ # @api private
28
+ # @since 0.1.0
29
+ def subclasses
30
+ @subclasses ||= Set.new
31
+ end
32
+
33
+ protected
34
+ # Loading mechanism hook.
35
+ #
36
+ # @api private
37
+ # @since 0.1.0
38
+ #
39
+ # @see Hanami::View.load!
40
+ def load!
41
+ subclasses.freeze
42
+ views.freeze
43
+ end
44
+
45
+ # Registered views
46
+ #
47
+ # @api private
48
+ # @since 0.1.0
49
+ def views
50
+ @views ||= [ self ] + subclasses.to_a
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,265 @@
1
+ require 'hanami/view/rendering/registry'
2
+ require 'hanami/view/rendering/scope'
3
+
4
+ module Hanami
5
+ module View
6
+ # Rendering methods
7
+ #
8
+ # @since 0.1.0
9
+ #
10
+ # @see Hanami::View::Rendering::InstanceMethods
11
+ module Rendering
12
+ def self.extended(base)
13
+ base.class_eval do
14
+ include InstanceMethods
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ # Initialize a view
20
+ #
21
+ # @param template [Hanami::View::Template] the template to render
22
+ # @param locals [Hash] a set of objects available during the rendering
23
+ # process.
24
+ #
25
+ # @since 0.1.0
26
+ #
27
+ # @see Hanami::View::Template
28
+ #
29
+ # @example
30
+ # require 'hanami/view'
31
+ #
32
+ # class IndexView
33
+ # include Hanami::View
34
+ # end
35
+ #
36
+ # template = Hanami::View::Template.new('index.html.erb')
37
+ # view = IndexView.new(template, {article: article})
38
+ def initialize(template, locals)
39
+ @template = template
40
+ @locals = locals
41
+ @scope = Scope.new(self, @locals)
42
+ end
43
+
44
+ # Render the template by bounding the local scope.
45
+ # If it uses a layout, it renders the template first and then the
46
+ # control passes to the layout.
47
+ #
48
+ # Override this method for custom rendering policies.
49
+ # For instance, when a serializer is used and there isn't the need of
50
+ # a template.
51
+ #
52
+ # @return [String] the output of the rendering process
53
+ #
54
+ # @raise [Hanami::View::MissingTemplateError] if the template is nil
55
+ #
56
+ # @since 0.1.0
57
+ #
58
+ # @see Hanami::View::Layout
59
+ #
60
+ # @example with template
61
+ # require 'hanami/view'
62
+ #
63
+ # class IndexView
64
+ # include Hanami::View
65
+ # end
66
+ #
67
+ # template = Hanami::View::Template.new('index.html.erb')
68
+ # view = IndexView.new(template, {article: article})
69
+ #
70
+ # view.render # => <h1>Introducing Hanami::view</h1> ...
71
+ #
72
+ # @example with template and layout
73
+ # require 'hanami/view'
74
+ #
75
+ # class ApplicationLayout
76
+ # include Hanami::View::Layout
77
+ # end
78
+ #
79
+ # class IndexView
80
+ # include Hanami::View
81
+ # layout :application
82
+ # end
83
+ #
84
+ # template = Hanami::View::Template.new('index.html.erb')
85
+ # view = IndexView.new(template, {article: article})
86
+ #
87
+ # view.render # => <html> ... <h1>Introducing Hanami::view</h1> ...
88
+ #
89
+ # @example with custom rendering
90
+ # require 'hanami/view'
91
+ #
92
+ # class IndexView
93
+ # include Hanami::View
94
+ #
95
+ # def render
96
+ # ArticleSerializer.new(article).render
97
+ # end
98
+ # end
99
+ #
100
+ # view = IndexView.new(nil, {article: article})
101
+ #
102
+ # view.render # => {title: ...}
103
+ def render
104
+ layout.render
105
+ end
106
+
107
+ protected
108
+ # The output of the template rendering process.
109
+ #
110
+ # @return [String] the rendering output
111
+ #
112
+ # @raise [Hanami::View::MissingTemplateError] if the template is nil
113
+ #
114
+ # @api private
115
+ # @since 0.1.0
116
+ def rendered
117
+ template.render @scope
118
+ end
119
+
120
+ # The layout.
121
+ #
122
+ # @return [Class, Hanami::View::Rendering::NullLayout]
123
+ #
124
+ # @see Hanami::View::Layout
125
+ # @see Hanami::View.layout
126
+ # @see Hanami::View::Dsl#layout
127
+ #
128
+ # @api private
129
+ # @since 0.1.0
130
+ def layout
131
+ @layout ||= self.class.layout.new(@scope, rendered)
132
+ end
133
+
134
+ # The template.
135
+ #
136
+ # @return [Hanami::View::Template] the template
137
+ #
138
+ # @raise [Hanami::View::MissingTemplateError] if the template is nil
139
+ #
140
+ # @api private
141
+ # @since 0.1.0
142
+ def template
143
+ @template or raise MissingTemplateError.new(self.class.template, @scope.format)
144
+ end
145
+
146
+ # A set of objects available during the rendering process.
147
+ #
148
+ # @return [Hash]
149
+ #
150
+ # @see Hanami::View#initialize
151
+ #
152
+ # @api private
153
+ # @since 0.1.0
154
+ def locals
155
+ @locals
156
+ end
157
+
158
+ # Delegates missing methods to the scope.
159
+ #
160
+ # @see Hanami::View::Rendering::Scope
161
+ #
162
+ # @api private
163
+ # @since 0.1.0
164
+ #
165
+ # @example
166
+ # require 'hanami/view'
167
+ #
168
+ # class IndexView
169
+ # include Hanami::View
170
+ # end
171
+ #
172
+ # template = Hanami::View::Template.new('index.html.erb')
173
+ # view = IndexView.new(template, {article: article})
174
+ #
175
+ # view.article # => #<Article:0x007fb0bbd3b6e8>
176
+ def method_missing(m)
177
+ @scope.__send__ m
178
+ end
179
+ end
180
+
181
+ # Render the given context and locals with the appropriate template.
182
+ # If there are registered subclasses, it choose the right class, according
183
+ # to the requested format.
184
+ #
185
+ # @param context [Hash] the context for the rendering process
186
+ # @option context [Symbol] :format the requested format
187
+ #
188
+ # @return [String] the output of the rendering process
189
+ #
190
+ # @raise [Hanami::View::MissingTemplateError] if it can't find a template
191
+ # for the given context
192
+ #
193
+ # @raise [Hanami::View::MissingFormatError] if the given context doesn't
194
+ # have the :format key
195
+ #
196
+ # @since 0.1.0
197
+ #
198
+ # @see Hanami::View#initialize
199
+ # @see Hanami::View#render
200
+ #
201
+ # @example
202
+ # require 'hanami/view'
203
+ #
204
+ # article = OpenStruct.new(title: 'Hello')
205
+ #
206
+ # module Articles
207
+ # class Show
208
+ # include Hanami::View
209
+ #
210
+ # def title
211
+ # @title ||= article.title.upcase
212
+ # end
213
+ # end
214
+ #
215
+ # class JsonShow < Show
216
+ # format :json
217
+ #
218
+ # def title
219
+ # super.downcase
220
+ # end
221
+ # end
222
+ # end
223
+ #
224
+ # Hanami::View.root = '/path/to/templates'
225
+ # Hanami::View.load!
226
+ #
227
+ # Articles::Show.render(format: :html, article: article)
228
+ # # => renders `articles/show.html.erb`
229
+ #
230
+ # Articles::Show.render(format: :json, article: article)
231
+ # # => renders `articles/show.json.erb`
232
+ #
233
+ # Articles::Show.render(format: :xml, article: article)
234
+ # # => raises Hanami::View::MissingTemplateError
235
+ def render(context)
236
+ registry.resolve(context).render
237
+ end
238
+
239
+ protected
240
+
241
+ # Loading mechanism hook.
242
+ #
243
+ # @api private
244
+ # @since 0.1.0
245
+ #
246
+ # @see Hanami::View.load!
247
+ def load!
248
+ super
249
+ registry.freeze
250
+ end
251
+
252
+ private
253
+
254
+ # The registry that holds all the registered subclasses.
255
+ #
256
+ # @api private
257
+ # @since 0.1.0
258
+ #
259
+ # @see Hanami::View::Rendering::Registry
260
+ def registry
261
+ @registry ||= Registry.new(self)
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,128 @@
1
+ require 'hanami/utils/string'
2
+ require 'hanami/utils/class'
3
+ require 'hanami/view/rendering/null_layout'
4
+
5
+ module Hanami
6
+ module View
7
+ module Rendering
8
+ # Defines the logic to find a layout
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ #
13
+ # @see Hanami::Layout
14
+ class LayoutFinder
15
+ # Layout class name suffix
16
+ #
17
+ # @api private
18
+ # @since 0.1.0
19
+ SUFFIX = 'Layout'.freeze
20
+
21
+ # Find a layout from the given name.
22
+ #
23
+ # @param layout [Symbol,String,NilClass] layout name or nil if you want
24
+ # to fallback to the framework defaults (see `Hanami::View.layout`).
25
+ #
26
+ # @param namespace [Class,Module] a Ruby namespace where to lookup
27
+ #
28
+ # @return [Hanami::Layout] the layout for the given name or
29
+ # `Hanami::View.layout`
30
+ #
31
+ # @api private
32
+ # @since 0.1.0
33
+ #
34
+ # @example With given name
35
+ # require 'hanami/view'
36
+ #
37
+ # Hanami::View::Rendering::LayoutFinder.find(:article) # =>
38
+ # ArticleLayout
39
+ #
40
+ # @example With a class
41
+ # require 'hanami/view'
42
+ #
43
+ # Hanami::View::Rendering::LayoutFinder.find(ArticleLayout) # =>
44
+ # ArticleLayout
45
+ #
46
+ # @example With namespace
47
+ # require 'hanami/view'
48
+ #
49
+ # Hanami::View::Rendering::LayoutFinder.find(:application, CardDeck) # =>
50
+ # CardDeck::ApplicationLayout
51
+ #
52
+ # @example With nil
53
+ # require 'hanami/view'
54
+ #
55
+ # Hanami::View::Rendering::LayoutFinder.find(nil) # =>
56
+ # Hanami::View::Rendering::NullLayout
57
+ #
58
+ # @example With unknown layout
59
+ # require 'hanami/view'
60
+ #
61
+ # Hanami::View::Rendering::LayoutFinder.find(:unknown) # =>
62
+ # Hanami::View::Rendering::NullLayout
63
+ #
64
+ def self.find(layout, namespace = Object)
65
+ case layout
66
+ when Symbol, String
67
+ # TODO Move this low level logic into a Hanami::Utils solution
68
+ class_name = "#{ Utils::String.new(layout).classify }#{ SUFFIX }"
69
+ namespace = Utils::Class.load_from_pattern!(namespace)
70
+ namespace.const_get(class_name)
71
+ when Class
72
+ layout
73
+ end || NullLayout
74
+ end
75
+
76
+ # Initialize the finder
77
+ #
78
+ # @param view [Class, #layout]
79
+ #
80
+ # @api private
81
+ # @since 0.1.0
82
+ def initialize(view)
83
+ @view = view
84
+ end
85
+
86
+ # Find the layout for the view
87
+ #
88
+ # @return [Hanami::Layout] the layout associated to the view
89
+ #
90
+ # @see Hanami::View::Rendering::LayoutFinder.find
91
+ # @see Hanami::View::Rendering::LayoutFinder#initialize
92
+ #
93
+ # @api private
94
+ # @since 0.1.0
95
+ #
96
+ # @example With layout
97
+ # require 'hanami/view'
98
+ #
99
+ # module Articles
100
+ # class Show
101
+ # include Hanami::View
102
+ # layout :article
103
+ # end
104
+ # end
105
+ #
106
+ # Hanami::View::Rendering::LayoutFinder.new(Articles::Show) # =>
107
+ # ArticleLayout
108
+ #
109
+ # @example Without layout
110
+ # require 'hanami/view'
111
+ #
112
+ # module Dashboard
113
+ # class Index
114
+ # include Hanami::View
115
+ # end
116
+ # end
117
+ #
118
+ # Hanami::View.layout # => :application
119
+ #
120
+ # Hanami::View::Rendering::LayoutFinder.new(Dashboard::Index) # =>
121
+ # ApplicationLayout
122
+ def find
123
+ self.class.find(@view.layout)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,63 @@
1
+ require 'hanami/view/rendering/null_template'
2
+ require 'hanami/view/rendering/templates_finder'
3
+
4
+ module Hanami
5
+ module View
6
+ module Rendering
7
+ # Holds the references of all the registered layouts.
8
+ # As now the registry is unique at the level of the framework.
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ #
13
+ # @see Hanami::Layout::ClassMethods#registry
14
+ class LayoutRegistry
15
+ # Initialize the registry
16
+ #
17
+ # @param view [Class] the view
18
+ #
19
+ # @api private
20
+ # @since 0.1.0
21
+ def initialize(view)
22
+ @registry = {}
23
+ @view = view
24
+ prepare!
25
+ end
26
+
27
+ # Returns the layout for the given context.
28
+ #
29
+ # @param context [Hash] the rendering context
30
+ # @option context [Symbol] :format the requested format
31
+ #
32
+ # @return [Hanami::Layout, Hanami::View::Rendering::NullTemplate]
33
+ # the layout associated with the given context or a `NullTemplate` if
34
+ # it can't be found.
35
+ #
36
+ # @raise [Hanami::View::MissingFormatError] if the given context doesn't
37
+ # have the :format key
38
+ #
39
+ # @api private
40
+ # @since 0.1.0
41
+ def resolve(context)
42
+ @registry.fetch(format(context)) { NullTemplate.new }
43
+ end
44
+
45
+ protected
46
+ def prepare!
47
+ templates.each do |template|
48
+ @registry.merge! template.format => template
49
+ end
50
+ @registry.any? or raise MissingTemplateLayoutError.new(@view)
51
+ end
52
+
53
+ def templates
54
+ TemplatesFinder.new(@view).find
55
+ end
56
+
57
+ def format(context)
58
+ context.fetch(:format) { raise MissingFormatError }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end