lotus-view 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -15
  3. data/.travis.yml +6 -0
  4. data/.yardopts +3 -0
  5. data/Gemfile +13 -2
  6. data/README.md +514 -3
  7. data/Rakefile +17 -1
  8. data/lib/lotus/layout.rb +132 -0
  9. data/lib/lotus/presenter.rb +70 -0
  10. data/lib/lotus/view/dsl.rb +247 -0
  11. data/lib/lotus/view/inheritable.rb +50 -0
  12. data/lib/lotus/view/rendering/layout_finder.rb +104 -0
  13. data/lib/lotus/view/rendering/layout_registry.rb +63 -0
  14. data/lib/lotus/view/rendering/layout_scope.rb +138 -0
  15. data/lib/lotus/view/rendering/null_layout.rb +52 -0
  16. data/lib/lotus/view/rendering/null_template.rb +79 -0
  17. data/lib/lotus/view/rendering/partial.rb +29 -0
  18. data/lib/lotus/view/rendering/partial_finder.rb +41 -0
  19. data/lib/lotus/view/rendering/registry.rb +129 -0
  20. data/lib/lotus/view/rendering/scope.rb +48 -0
  21. data/lib/lotus/view/rendering/template.rb +56 -0
  22. data/lib/lotus/view/rendering/template_finder.rb +53 -0
  23. data/lib/lotus/view/rendering/templates_finder.rb +85 -0
  24. data/lib/lotus/view/rendering/view_finder.rb +37 -0
  25. data/lib/lotus/view/rendering.rb +265 -0
  26. data/lib/lotus/view/template.rb +45 -0
  27. data/lib/lotus/view/version.rb +4 -1
  28. data/lib/lotus/view.rb +180 -2
  29. data/lib/lotus-view.rb +1 -0
  30. data/lotus-view.gemspec +15 -11
  31. data/test/fixtures/templates/app/app_view.html.erb +0 -0
  32. data/test/fixtures/templates/app/view.html.erb +0 -0
  33. data/test/fixtures/templates/application.html.erb +10 -0
  34. data/test/fixtures/templates/articles/_form.html.erb +4 -0
  35. data/test/fixtures/templates/articles/alternative_new.html.erb +1 -0
  36. data/test/fixtures/templates/articles/index.atom.erb +5 -0
  37. data/test/fixtures/templates/articles/index.html.erb +3 -0
  38. data/test/fixtures/templates/articles/index.json.erb +9 -0
  39. data/test/fixtures/templates/articles/index.rss.erb +0 -0
  40. data/test/fixtures/templates/articles/new.html.erb +7 -0
  41. data/test/fixtures/templates/articles/show.html.erb +1 -0
  42. data/test/fixtures/templates/articles/show.json.erb +5 -0
  43. data/test/fixtures/templates/contacts/show.html.haml +1 -0
  44. data/test/fixtures/templates/dashboard/index.html.erb +2 -0
  45. data/test/fixtures/templates/hello_world_view.html.erb +1 -0
  46. data/test/fixtures/templates/index_view.html.erb +1 -0
  47. data/test/fixtures/templates/json_render_view.json.erb +3 -0
  48. data/test/fixtures/templates/render_view.html.erb +1 -0
  49. data/test/fixtures/templates/shared/_sidebar.html.erb +1 -0
  50. data/test/fixtures.rb +187 -0
  51. data/test/layout_test.rb +10 -0
  52. data/test/load_test.rb +79 -0
  53. data/test/presenter_test.rb +31 -0
  54. data/test/rendering_test.rb +125 -0
  55. data/test/root_test.rb +38 -0
  56. data/test/test_helper.rb +24 -0
  57. data/test/version_test.rb +7 -0
  58. data/test/view_test.rb +27 -0
  59. metadata +137 -10
@@ -0,0 +1,3 @@
1
+ <% articles.each do |article| %>
2
+ <h1><%= article.title %></h1>
3
+ <% end %>
@@ -0,0 +1,9 @@
1
+ {
2
+ "articles": [
3
+ <% articles.each do |article| %>
4
+ "article":{
5
+ "title":"<%= article.title %>"
6
+ }
7
+ <% end %>
8
+ ]
9
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ <h1>New Article</h1>
2
+
3
+ <% if errors.any? %>
4
+ <h2>Errors</h2>
5
+ <% end %>
6
+
7
+ <%= render partial: 'articles/form', locals: { secret: 23 } %>
@@ -0,0 +1 @@
1
+ <h1><%= title %></h1>
@@ -0,0 +1,5 @@
1
+ {
2
+ article: {
3
+ "title":"<%= title %>"
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ %h1= person.name
@@ -0,0 +1,2 @@
1
+ <h1>Map</h1>
2
+ <h2><%= map.count %> locations</h2>
@@ -0,0 +1 @@
1
+ <h1>Hello, World!</h1>
@@ -0,0 +1 @@
1
+ <h1>Welcome</h1>
@@ -0,0 +1,3 @@
1
+ {
2
+ "greet":"Hello, <%= planet %>!"
3
+ }
@@ -0,0 +1 @@
1
+ <h1>Hello, <%= planet %>!</h1>
@@ -0,0 +1 @@
1
+ <div id="sidebar"></div>
data/test/fixtures.rb ADDED
@@ -0,0 +1,187 @@
1
+ class HelloWorldView
2
+ include Lotus::View
3
+ end
4
+
5
+ class RenderView
6
+ include Lotus::View
7
+ end
8
+
9
+ class JsonRenderView
10
+ include Lotus::View
11
+ format :json
12
+ end
13
+
14
+ class AppView
15
+ include Lotus::View
16
+ root __dir__ + '/fixtures/templates/app'
17
+ layout :application
18
+ end
19
+
20
+ class AppViewLayout < AppView
21
+ layout nil
22
+ end
23
+
24
+ class MissingTemplateView
25
+ include Lotus::View
26
+ end
27
+
28
+ module App
29
+ class View
30
+ include Lotus::View
31
+ end
32
+ end
33
+
34
+ class ApplicationLayout
35
+ include Lotus::Layout
36
+
37
+ def title
38
+ 'Title:'
39
+ end
40
+ end
41
+
42
+ class GlobalLayout
43
+ end
44
+
45
+ module Articles
46
+ class Index
47
+ include Lotus::View
48
+ layout :application
49
+
50
+ def title
51
+ "#{ layout.title } articles"
52
+ end
53
+ end
54
+
55
+ class RssIndex < Index
56
+ format :rss
57
+ layout nil
58
+ end
59
+
60
+ class AtomIndex < RssIndex
61
+ format :atom
62
+ layout nil
63
+ end
64
+
65
+ class New
66
+ include Lotus::View
67
+
68
+ def errors
69
+ {}
70
+ end
71
+ end
72
+
73
+ class AlternativeNew
74
+ include Lotus::View
75
+ end
76
+
77
+ class Create
78
+ include Lotus::View
79
+ template 'articles/new'
80
+
81
+ def errors
82
+ {title: 'Title is required'}
83
+ end
84
+ end
85
+
86
+ class Show
87
+ include Lotus::View
88
+
89
+ def title
90
+ @title ||= article.title.upcase
91
+ end
92
+ end
93
+
94
+ class JsonShow < Show
95
+ format :json
96
+
97
+ def article
98
+ OpenStruct.new(title: locals[:article].title.reverse)
99
+ end
100
+
101
+ def title
102
+ super.downcase
103
+ end
104
+ end
105
+ end
106
+
107
+ class Map
108
+ attr_reader :locations
109
+
110
+ def initialize(locations)
111
+ @locations = locations
112
+ end
113
+
114
+ def location_names
115
+ @locations.join(', ')
116
+ end
117
+ end
118
+
119
+ class MapPresenter
120
+ include Lotus::Presenter
121
+
122
+ def count
123
+ locations.count
124
+ end
125
+
126
+ def location_names
127
+ super.upcase
128
+ end
129
+
130
+ def inspect_object
131
+ @object.inspect
132
+ end
133
+ end
134
+
135
+ module Dashboard
136
+ class Index
137
+ include Lotus::View
138
+
139
+ def map
140
+ MapPresenter.new(locals[:map])
141
+ end
142
+ end
143
+ end
144
+
145
+ class IndexView
146
+ include Lotus::View
147
+ layout :application
148
+ end
149
+
150
+ class SongWidget
151
+ attr_reader :song
152
+
153
+ def initialize(song)
154
+ @song = song
155
+ end
156
+
157
+ def render
158
+ %(<audio src="#{ song.url }">#{ song.title }</audio>)
159
+ end
160
+ end
161
+
162
+ module Songs
163
+ class Show
164
+ include Lotus::View
165
+ format :html
166
+
167
+ def render
168
+ SongWidget.new(song).render
169
+ end
170
+ end
171
+ end
172
+
173
+ module Metrics
174
+ class Index
175
+ include Lotus::View
176
+
177
+ def render
178
+ %(metrics)
179
+ end
180
+ end
181
+ end
182
+
183
+ module Contacts
184
+ class Show
185
+ include Lotus::View
186
+ end
187
+ end
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::Layout do
4
+ describe 'rendering from layout' do
5
+ it 'renders partial' do
6
+ rendered = IndexView.render(format: :html)
7
+ rendered.must_match %(<div id="sidebar"></div>)
8
+ end
9
+ end
10
+ end
data/test/load_test.rb ADDED
@@ -0,0 +1,79 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::View do
4
+ describe '.load!' do
5
+ before do
6
+ # Lotus::View.load! is invoked as last statement of `test/test_helper.rb`.
7
+ end
8
+
9
+ it 'freezes .root' do
10
+ Lotus::View.root.frozen?.must_equal true
11
+ end
12
+
13
+ it 'freezes .root for all the views' do
14
+ AppView.root.frozen?.must_equal true
15
+ end
16
+
17
+ it 'freezes .layout for all the views' do
18
+ AppView.layout.frozen?.must_equal true
19
+ end
20
+
21
+ it 'freezes .layout for subclasses' do
22
+ AppViewLayout.layout.frozen?.must_equal true
23
+ end
24
+
25
+ it 'freezes .views' do
26
+ Lotus::View.views.frozen?.must_equal true
27
+ end
28
+
29
+ it 'freezes .format for all the views with that declaration' do
30
+ JsonRenderView.format.frozen?.must_equal true
31
+ end
32
+
33
+ it 'freezes .format for subclasses' do
34
+ Articles::RssIndex.format.frozen?.must_equal true
35
+ end
36
+
37
+ it 'freezes .template' do
38
+ Articles::Show.template.frozen?.must_equal true
39
+ end
40
+
41
+ it 'freezes .template for subclasses' do
42
+ Articles::JsonShow.template.frozen?.must_equal true
43
+ end
44
+
45
+ it 'freezes .subclasses' do
46
+ Articles::Index.subclasses.frozen?.must_equal true
47
+ end
48
+
49
+ it 'freezes .subclasses for subclasses' do
50
+ Articles::AtomIndex.subclasses.frozen?.must_equal true
51
+ end
52
+
53
+ it 'freezes view .views' do
54
+ Articles::Index.send(:views).frozen?.must_equal true
55
+ end
56
+
57
+ it 'freezes .views for subclasses' do
58
+ Articles::RssIndex.send(:views).frozen?.must_equal true
59
+ end
60
+
61
+ it 'freezes .registry' do
62
+ Articles::Index.send(:registry).frozen?.must_equal true
63
+ end
64
+
65
+ it 'freezes .registry for subclasses' do
66
+ Articles::AtomIndex.send(:registry).frozen?.must_equal true
67
+ end
68
+
69
+ describe 'layouts' do
70
+ it 'freezes .root' do
71
+ ApplicationLayout.root.frozen?.must_equal true
72
+ end
73
+
74
+ it 'freezes .registry' do
75
+ ApplicationLayout.send(:registry).frozen?.must_equal true
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::Presenter do
4
+ subject do
5
+ MapPresenter.new(map)
6
+ end
7
+
8
+ let(:map) { Map.new(['Rome']) }
9
+
10
+ it 'forwards methods to the wrapped object' do
11
+ subject.locations.must_equal map.locations
12
+ end
13
+
14
+ it 'uses concrete methods' do
15
+ subject.count.must_equal map.locations.count
16
+ end
17
+
18
+ it 'uses super to access object implementation' do
19
+ subject.location_names.must_equal map.locations.map {|l| l.upcase }.join(', ')
20
+ end
21
+
22
+ it 'has a direct access to the object' do
23
+ subject.inspect_object.must_match '#<Map'
24
+ end
25
+
26
+ it "raises error when the requested method can't be satisfied" do
27
+ -> {
28
+ subject.unknown_method
29
+ }.must_raise NoMethodError
30
+ end
31
+ end
@@ -0,0 +1,125 @@
1
+ require 'test_helper'
2
+ require 'ostruct'
3
+
4
+ describe Lotus::View do
5
+ describe 'rendering' do
6
+ it 'renders a template' do
7
+ HelloWorldView.render(format: :html).must_include %(<h1>Hello, World!</h1>)
8
+ end
9
+
10
+ it 'renders a template with context binding' do
11
+ RenderView.render(format: :html, planet: 'Mars').must_include %(<h1>Hello, Mars!</h1>)
12
+ end
13
+
14
+ it 'renders a template according to the declared format' do
15
+ JsonRenderView.render(format: :json, planet: 'Moon').must_include %("greet":"Hello, Moon!")
16
+ end
17
+
18
+ it 'renders a template according to the requested format' do
19
+ articles = [ OpenStruct.new(title: 'Man on the Moon!') ]
20
+
21
+ rendered = Articles::Index.render(format: :json, articles: articles)
22
+ rendered.must_match %("title":"Man on the Moon!")
23
+
24
+ rendered = Articles::Index.render(format: :html, articles: articles)
25
+ rendered.must_match %(<h1>Man on the Moon!</h1>)
26
+ end
27
+
28
+ it 'binds given locals to the rendering context' do
29
+ article = OpenStruct.new(title: 'Hello')
30
+
31
+ rendered = Articles::Show.render(format: :html, article: article)
32
+ rendered.must_match %(<h1>HELLO</h1>)
33
+ end
34
+
35
+ it 'renders a template from a subclass, if it is able to handle the requested format' do
36
+ article = OpenStruct.new(title: 'Hello')
37
+
38
+ rendered = Articles::Show.render(format: :json, article: article)
39
+ rendered.must_match %("title":"olleh")
40
+ end
41
+
42
+ it 'raises an error when the template is missing' do
43
+ article = OpenStruct.new(title: 'Ciao')
44
+
45
+ -> {
46
+ Articles::Show.render(format: :png, article: article)
47
+ }.must_raise(Lotus::View::MissingTemplateError)
48
+ end
49
+
50
+ it 'raises an error when the format is missing' do
51
+ -> {
52
+ HelloWorldView.render({})
53
+ }.must_raise(Lotus::View::MissingFormatError)
54
+ end
55
+
56
+ it 'renders different template, as specified by DSL' do
57
+ article = OpenStruct.new(title: 'Bonjour')
58
+
59
+ rendered = Articles::Create.render(format: :html, article: article)
60
+ rendered.must_match %(<h1>New Article</h1>)
61
+ rendered.must_match %(<h2>Errors</h2>)
62
+ end
63
+
64
+ it 'decorates locals' do
65
+ map = Map.new(['Rome', 'Cambridge'])
66
+
67
+ rendered = Dashboard::Index.render(format: :html, map: map)
68
+ rendered.must_match %(<h1>Map</h1>)
69
+ rendered.must_match %(<h2>2 locations</h2>)
70
+ end
71
+
72
+ it 'renders a partial' do
73
+ article = OpenStruct.new(title: nil)
74
+
75
+ rendered = Articles::New.render(format: :html, article: article)
76
+
77
+ rendered.must_match %(<h1>New Article</h1>)
78
+ rendered.must_match %(<input type="hidden" name="secret" value="23" />)
79
+ end
80
+
81
+ it 'renders a template within another template' do
82
+ article = OpenStruct.new(title: nil)
83
+
84
+ rendered = Articles::AlternativeNew.render(format: :html, article: article)
85
+
86
+ rendered.must_match %(<h1>New Article</h1>)
87
+ rendered.must_match %(<input type="hidden" name="secret" value="23" />)
88
+ end
89
+
90
+ it 'uses HAML engine' do
91
+ person = OpenStruct.new(name: 'Luca')
92
+
93
+ rendered = Contacts::Show.render(format: :html, person: person)
94
+ rendered.must_match %(<h1>Luca</h1>)
95
+ end
96
+
97
+ describe 'when without a template' do
98
+ it 'renders from the custom rendering method' do
99
+ song = OpenStruct.new(title: 'Song Two', url: '/song2.mp3')
100
+
101
+ rendered = Songs::Show.render(format: :html, song: song)
102
+ rendered.must_equal %(<audio src="/song2.mp3">Song Two</audio>)
103
+ end
104
+
105
+ it 'respond to all the formats' do
106
+ rendered = Metrics::Index.render(format: :html)
107
+ rendered.must_equal %(metrics)
108
+
109
+ rendered = Metrics::Index.render(format: :json)
110
+ rendered.must_equal %(metrics)
111
+ end
112
+ end
113
+
114
+ describe 'layout' do
115
+ it 'renders contents from layout' do
116
+ articles = [ OpenStruct.new(title: 'A Wonderful Day!') ]
117
+
118
+ rendered = Articles::Index.render(format: :html, articles: articles)
119
+ rendered.must_match %(<h1>A Wonderful Day!</h1>)
120
+ rendered.must_match %(<html>)
121
+ rendered.must_match %(<title>Title: articles</title>)
122
+ end
123
+ end
124
+ end
125
+ end
data/test/root_test.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::View do
4
+ describe 'root' do
5
+ before do
6
+ Lotus::View.root = Pathname.new __dir__ + '/fixtures/templates'
7
+ end
8
+
9
+ it 'exposes the path where to lookup for templates' do
10
+ expected = Pathname.new __dir__ + '/fixtures/templates'
11
+ Lotus::View.root.must_equal expected
12
+ end
13
+
14
+ it 'is inherited' do
15
+ HelloWorldView.root.must_equal Lotus::View.root
16
+ end
17
+
18
+ it 'can be customized for each view' do
19
+ expected = Pathname.new __dir__ + '/fixtures/templates/app'
20
+ AppView.root.must_equal expected
21
+ end
22
+
23
+ describe 'when not set' do
24
+ before do
25
+ @root = Lotus::View.root
26
+ Lotus::View.root = nil
27
+ end
28
+
29
+ after do
30
+ Lotus::View.root = @root
31
+ end
32
+
33
+ it 'sets the current directory as root' do
34
+ Lotus::View.root.must_equal Pathname.new('.')
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ if ENV['COVERAGE'] == 'true'
5
+ require 'simplecov'
6
+ require 'coveralls'
7
+
8
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
9
+ SimpleCov::Formatter::HTMLFormatter,
10
+ Coveralls::SimpleCov::Formatter
11
+ ]
12
+
13
+ SimpleCov.start do
14
+ command_name 'test'
15
+ add_filter 'test'
16
+ end
17
+ end
18
+
19
+ require 'minitest/autorun'
20
+ $:.unshift 'lib'
21
+ require 'lotus/view'
22
+ Lotus::View.root = Pathname.new __dir__ + '/fixtures/templates'
23
+ require 'fixtures'
24
+ Lotus::View.load!
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::View::VERSION do
4
+ it 'returns current version' do
5
+ Lotus::View::VERSION.must_equal '0.1.0'
6
+ end
7
+ end
data/test/view_test.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::View do
4
+ describe '.layout=' do
5
+ before do
6
+ Lotus::View.unload!
7
+
8
+ class ViewWithInheritedLayout
9
+ include Lotus::View
10
+ end
11
+
12
+ Lotus::View.layout = :application
13
+ Lotus::View.load!
14
+ end
15
+
16
+ after do
17
+ Lotus::View.unload!
18
+ Object.send(:remove_const, :ViewWithInheritedLayout)
19
+ Lotus::View.layout = nil
20
+ Lotus::View.load!
21
+ end
22
+
23
+ it 'sets global layout' do
24
+ ViewWithInheritedLayout.layout.must_equal ApplicationLayout
25
+ end
26
+ end
27
+ end