curlybars 0.9.13

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/lib/curlybars.rb +108 -0
  3. data/lib/curlybars/configuration.rb +41 -0
  4. data/lib/curlybars/dependency_tracker.rb +8 -0
  5. data/lib/curlybars/error/base.rb +18 -0
  6. data/lib/curlybars/error/compile.rb +11 -0
  7. data/lib/curlybars/error/lex.rb +22 -0
  8. data/lib/curlybars/error/parse.rb +41 -0
  9. data/lib/curlybars/error/presenter/not_found.rb +23 -0
  10. data/lib/curlybars/error/render.rb +11 -0
  11. data/lib/curlybars/error/validate.rb +18 -0
  12. data/lib/curlybars/lexer.rb +60 -0
  13. data/lib/curlybars/method_whitelist.rb +69 -0
  14. data/lib/curlybars/node/block_helper_else.rb +108 -0
  15. data/lib/curlybars/node/boolean.rb +24 -0
  16. data/lib/curlybars/node/each_else.rb +69 -0
  17. data/lib/curlybars/node/if_else.rb +33 -0
  18. data/lib/curlybars/node/item.rb +31 -0
  19. data/lib/curlybars/node/literal.rb +28 -0
  20. data/lib/curlybars/node/option.rb +25 -0
  21. data/lib/curlybars/node/output.rb +24 -0
  22. data/lib/curlybars/node/partial.rb +24 -0
  23. data/lib/curlybars/node/path.rb +137 -0
  24. data/lib/curlybars/node/root.rb +29 -0
  25. data/lib/curlybars/node/string.rb +24 -0
  26. data/lib/curlybars/node/template.rb +32 -0
  27. data/lib/curlybars/node/text.rb +24 -0
  28. data/lib/curlybars/node/unless_else.rb +33 -0
  29. data/lib/curlybars/node/variable.rb +34 -0
  30. data/lib/curlybars/node/with_else.rb +54 -0
  31. data/lib/curlybars/parser.rb +183 -0
  32. data/lib/curlybars/position.rb +7 -0
  33. data/lib/curlybars/presenter.rb +288 -0
  34. data/lib/curlybars/processor/tilde.rb +31 -0
  35. data/lib/curlybars/processor/token_factory.rb +9 -0
  36. data/lib/curlybars/railtie.rb +18 -0
  37. data/lib/curlybars/rendering_support.rb +222 -0
  38. data/lib/curlybars/safe_buffer.rb +11 -0
  39. data/lib/curlybars/template_handler.rb +93 -0
  40. data/lib/curlybars/version.rb +3 -0
  41. data/spec/acceptance/application_layout_spec.rb +60 -0
  42. data/spec/acceptance/collection_blocks_spec.rb +28 -0
  43. data/spec/acceptance/global_helper_spec.rb +25 -0
  44. data/spec/curlybars/configuration_spec.rb +57 -0
  45. data/spec/curlybars/error/base_spec.rb +41 -0
  46. data/spec/curlybars/error/compile_spec.rb +19 -0
  47. data/spec/curlybars/error/lex_spec.rb +25 -0
  48. data/spec/curlybars/error/parse_spec.rb +74 -0
  49. data/spec/curlybars/error/render_spec.rb +19 -0
  50. data/spec/curlybars/error/validate_spec.rb +19 -0
  51. data/spec/curlybars/lexer_spec.rb +466 -0
  52. data/spec/curlybars/method_whitelist_spec.rb +168 -0
  53. data/spec/curlybars/processor/tilde_spec.rb +60 -0
  54. data/spec/curlybars/rendering_support_spec.rb +426 -0
  55. data/spec/curlybars/safe_buffer_spec.rb +46 -0
  56. data/spec/curlybars/template_handler_spec.rb +222 -0
  57. data/spec/integration/cache_spec.rb +124 -0
  58. data/spec/integration/comment_spec.rb +60 -0
  59. data/spec/integration/exception_spec.rb +31 -0
  60. data/spec/integration/node/block_helper_else_spec.rb +422 -0
  61. data/spec/integration/node/each_else_spec.rb +204 -0
  62. data/spec/integration/node/each_spec.rb +291 -0
  63. data/spec/integration/node/escape_spec.rb +27 -0
  64. data/spec/integration/node/helper_spec.rb +176 -0
  65. data/spec/integration/node/if_else_spec.rb +129 -0
  66. data/spec/integration/node/if_spec.rb +143 -0
  67. data/spec/integration/node/output_spec.rb +68 -0
  68. data/spec/integration/node/partial_spec.rb +66 -0
  69. data/spec/integration/node/path_spec.rb +286 -0
  70. data/spec/integration/node/root_spec.rb +15 -0
  71. data/spec/integration/node/template_spec.rb +86 -0
  72. data/spec/integration/node/unless_else_spec.rb +129 -0
  73. data/spec/integration/node/unless_spec.rb +130 -0
  74. data/spec/integration/node/with_spec.rb +116 -0
  75. data/spec/integration/processor/tilde_spec.rb +38 -0
  76. data/spec/integration/processors_spec.rb +30 -0
  77. metadata +358 -0
@@ -0,0 +1,29 @@
1
+ module Curlybars
2
+ module Node
3
+ Root = Struct.new(:template, :position) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ contexts = [presenter]
9
+ variables = [{}]
10
+ rendering = ::Curlybars::RenderingSupport.new(
11
+ ::Curlybars.configuration.rendering_timeout,
12
+ contexts,
13
+ variables,
14
+ #{position.file_name.inspect},
15
+ global_helpers_providers,
16
+ ::Curlybars.configuration.cache
17
+ )
18
+ buffer = ::Curlybars::SafeBuffer.new
19
+ #{template.compile}
20
+ buffer
21
+ RUBY
22
+ end
23
+
24
+ def validate(branches)
25
+ template.validate(branches)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module Curlybars
2
+ module Node
3
+ String = Struct.new(:string) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ ->() { #{string.inspect} }
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ # Nothing to validate here.
14
+ end
15
+
16
+ def cache_key
17
+ [
18
+ string,
19
+ self.class.name
20
+ ].join("/")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ module Curlybars
2
+ module Node
3
+ Template = Struct.new(:items, :position) do
4
+ def compile
5
+ compiled_items = items.map(&:compile).join("\n")
6
+
7
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
8
+ # outputted by this node.
9
+ <<-RUBY
10
+ ::Module.new do
11
+ def self.exec(contexts, rendering, variables, buffer)
12
+ unless contexts.length < ::Curlybars.configuration.nesting_limit
13
+ message = "Nesting too deep"
14
+ position = rendering.position(#{position.line_number}, #{position.line_offset})
15
+ raise ::Curlybars::Error::Render.new('nesting_too_deep', message, position)
16
+ end
17
+ #{compiled_items}
18
+ end
19
+ end.exec(contexts, rendering, variables, buffer)
20
+ RUBY
21
+ end
22
+
23
+ def validate(branches)
24
+ items.map { |item| item.validate(branches) }
25
+ end
26
+
27
+ def cache_key
28
+ Digest::MD5.hexdigest(items.map(&:cache_key).join("/"))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ module Curlybars
2
+ module Node
3
+ Text = Struct.new(:text) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ buffer.concat(#{text.inspect}.html_safe)
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ # Nothing to validate here.
14
+ end
15
+
16
+ def cache_key
17
+ [
18
+ text.to_s,
19
+ self.class.name
20
+ ].join("/")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Curlybars
2
+ module Node
3
+ UnlessElse = Struct.new(:expression, :unless_template, :else_template) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ unless rendering.to_bool(rendering.cached_call(#{expression.compile}))
9
+ #{unless_template.compile}
10
+ else
11
+ #{else_template.compile}
12
+ end
13
+ RUBY
14
+ end
15
+
16
+ def validate(branches)
17
+ [
18
+ expression.validate(branches),
19
+ unless_template.validate(branches),
20
+ else_template.validate(branches)
21
+ ]
22
+ end
23
+
24
+ def cache_key
25
+ [
26
+ expression,
27
+ unless_template,
28
+ else_template
29
+ ].map(&:cache_key).push(self.class.name).join("/")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module Curlybars
2
+ module Node
3
+ Variable = Struct.new(:variable, :position) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ -> {
9
+ position = rendering.position(
10
+ #{position.line_number},
11
+ #{position.line_offset}
12
+ )
13
+ rendering.variable(#{variable.inspect}, position)
14
+ }
15
+ RUBY
16
+ end
17
+
18
+ def validate(branches)
19
+ # Nothing to validate here.
20
+ end
21
+
22
+ def validate_as_value(branches)
23
+ # It is always a value.
24
+ end
25
+
26
+ def cache_key
27
+ [
28
+ variable,
29
+ self.class.name
30
+ ].join("/")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ module Curlybars
2
+ module Node
3
+ WithElse = Struct.new(:path, :with_template, :else_template, :position) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ compiled_path = rendering.cached_call(#{path.compile})
9
+
10
+ if rendering.to_bool(compiled_path)
11
+ position = rendering.position(#{position.line_number}, #{position.line_offset})
12
+ rendering.check_context_is_presenter(compiled_path, #{path.path.inspect}, position)
13
+
14
+ contexts.push(compiled_path)
15
+ begin
16
+ #{with_template.compile}
17
+ ensure
18
+ contexts.pop
19
+ end
20
+ else
21
+ #{else_template.compile}
22
+ end
23
+ RUBY
24
+ end
25
+
26
+ def validate(branches)
27
+ sub_tree = path.resolve_and_check!(branches, check_type: :presenter)
28
+ with_template_errors = begin
29
+ branches.push(sub_tree)
30
+ with_template.validate(branches)
31
+ ensure
32
+ branches.pop
33
+ end
34
+
35
+ else_template_errors = else_template.validate(branches)
36
+
37
+ [
38
+ with_template_errors,
39
+ else_template_errors
40
+ ]
41
+ rescue Curlybars::Error::Validate => path_error
42
+ path_error
43
+ end
44
+
45
+ def cache_key
46
+ [
47
+ path,
48
+ with_template,
49
+ else_template
50
+ ].map(&:cache_key).push(self.class.name).join("/")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,183 @@
1
+ require 'rltk/parser'
2
+
3
+ require 'curlybars/node/root'
4
+ require 'curlybars/node/template'
5
+ require 'curlybars/node/item'
6
+ require 'curlybars/node/text'
7
+ require 'curlybars/node/if_else'
8
+ require 'curlybars/node/unless_else'
9
+ require 'curlybars/node/each_else'
10
+ require 'curlybars/node/path'
11
+ require 'curlybars/node/literal'
12
+ require 'curlybars/node/variable'
13
+ require 'curlybars/node/with_else'
14
+ require 'curlybars/node/block_helper_else'
15
+ require 'curlybars/node/option'
16
+ require 'curlybars/node/partial'
17
+ require 'curlybars/node/output'
18
+
19
+ module Curlybars
20
+ class Parser < RLTK::Parser
21
+ start :root
22
+
23
+ production(:root, 'template?') { |template| Node::Root.new(template || VOID, pos(0)) }
24
+ production(:template, 'items') { |items| Node::Template.new(items || [], pos(0)) }
25
+
26
+ production(:items) do
27
+ clause('items item') { |items, item| items << Node::Item.new(item) }
28
+ clause('item') { |item| [Node::Item.new(item)] }
29
+ end
30
+
31
+ production(:item) do
32
+ clause('TEXT') { |text| Node::Text.new(text) }
33
+
34
+ clause('START HASH .path .expressions? .options? END
35
+ .template?
36
+ START SLASH .path END') do |helper, arguments, options, template, helperclose|
37
+ Node::BlockHelperElse.new(
38
+ helper,
39
+ arguments || [],
40
+ options || [],
41
+ template || VOID,
42
+ VOID,
43
+ helperclose,
44
+ pos(0)
45
+ )
46
+ end
47
+
48
+ clause('START HASH .path .expressions? .options? END
49
+ .template?
50
+ START ELSE END
51
+ .template?
52
+ START SLASH .path END') do |helper, arguments, options, helper_template, else_template, helperclose|
53
+ Node::BlockHelperElse.new(
54
+ helper,
55
+ arguments || [],
56
+ options || [],
57
+ helper_template || VOID,
58
+ else_template || VOID,
59
+ helperclose,
60
+ pos(0)
61
+ )
62
+ end
63
+
64
+ clause('START .path .expressions? .options? END') do |helper, arguments, options|
65
+ Node::BlockHelperElse.new(
66
+ helper,
67
+ arguments || [],
68
+ options || [],
69
+ VOID,
70
+ VOID,
71
+ helper,
72
+ pos(0)
73
+ )
74
+ end
75
+
76
+ clause('START .value END') do |value|
77
+ Node::Output.new(value)
78
+ end
79
+
80
+ clause('START HASH IF .expression END
81
+ .template?
82
+ START SLASH IF END') do |expression, if_template|
83
+ Node::IfElse.new(expression, if_template || VOID, VOID)
84
+ end
85
+
86
+ clause('START HASH IF .expression END
87
+ .template?
88
+ START ELSE END
89
+ .template?
90
+ START SLASH IF END') do |expression, if_template, else_template|
91
+ Node::IfElse.new(expression, if_template || VOID, else_template || VOID)
92
+ end
93
+
94
+ clause('START HASH UNLESS .expression END
95
+ .template?
96
+ START SLASH UNLESS END') do |expression, unless_template|
97
+ Node::UnlessElse.new(expression, unless_template || VOID, VOID)
98
+ end
99
+
100
+ clause('START HASH UNLESS .expression END
101
+ .template?
102
+ START ELSE END
103
+ .template?
104
+ START SLASH UNLESS END') do |expression, unless_template, else_template|
105
+ Node::UnlessElse.new(expression, unless_template || VOID, else_template || VOID)
106
+ end
107
+
108
+ clause('START HASH EACH .path END
109
+ .template?
110
+ START SLASH EACH END') do |path, each_template|
111
+ Node::EachElse.new(path, each_template || VOID, VOID, pos(0))
112
+ end
113
+
114
+ clause('START HASH EACH .path END
115
+ .template?
116
+ START ELSE END
117
+ .template?
118
+ START SLASH EACH END') do |path, each_template, else_template|
119
+ Node::EachElse.new(path, each_template || VOID, else_template || VOID, pos(0))
120
+ end
121
+
122
+ clause('START HASH WITH .path END
123
+ .template?
124
+ START SLASH WITH END') do |path, with_template|
125
+ Node::WithElse.new(path, with_template || VOID, VOID, pos(0))
126
+ end
127
+
128
+ clause('START HASH WITH .path END
129
+ .template?
130
+ START ELSE END
131
+ .template?
132
+ START SLASH WITH END') do |path, with_template, else_template|
133
+ Node::WithElse.new(path, with_template || VOID, else_template || VOID, pos(0))
134
+ end
135
+
136
+ clause('START GT .path END') do |path|
137
+ Node::Partial.new(path)
138
+ end
139
+ end
140
+
141
+ production(:options) do
142
+ clause('options option') { |options, option| options << option }
143
+ clause('option') { |option| [option] }
144
+ end
145
+
146
+ production(:option, '.KEY .expression') do |key, expression|
147
+ Node::Option.new(key, expression)
148
+ end
149
+
150
+ production(:expressions) do
151
+ clause('expressions expression') { |expressions, expression| expressions << expression }
152
+ clause('expression') { |expression| [expression] }
153
+ end
154
+
155
+ production(:expression) do
156
+ clause('value') { |value| value }
157
+ clause('path') { |path| path }
158
+ end
159
+
160
+ production(:value) do
161
+ clause('LITERAL') { |literal| Node::Literal.new(literal) }
162
+ clause('VARIABLE') { |variable| Node::Variable.new(variable, pos(0)) }
163
+ end
164
+
165
+ production(:path, 'PATH') { |path| Node::Path.new(path, pos(0)) }
166
+
167
+ finalize
168
+
169
+ VOID = Class.new do
170
+ def compile
171
+ # Nothing to compile.
172
+ end
173
+
174
+ def validate(branches)
175
+ [] # Nothing to validate.
176
+ end
177
+
178
+ def cache_key
179
+ # Empty cache key.
180
+ end
181
+ end.new
182
+ end
183
+ end
@@ -0,0 +1,7 @@
1
+ module Curlybars
2
+ Position = Struct.new(:file_name, :line_number, :line_offset, :length) do
3
+ def initialize(file_name, line_number, line_offset, length = 0)
4
+ super
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,288 @@
1
+ require 'curlybars/method_whitelist'
2
+ require 'curlybars/configuration'
3
+
4
+ module Curlybars
5
+ # A base class that can be subclassed by concrete presenters.
6
+ #
7
+ # A Curlybars presenter is responsible for delivering data to templates, in the
8
+ # form of simple strings. Each public instance method on the presenter class
9
+ # can be referenced in a template. When a template is evaluated with a
10
+ # presenter, the referenced methods will be called with no arguments, and
11
+ # the returned strings inserted in place of the components in the template.
12
+ #
13
+ # Note that strings that are not HTML safe will be escaped.
14
+ #
15
+ # A presenter is always instantiated with a context to which it delegates
16
+ # unknown messages, usually an instance of ActionView::Base provided by
17
+ # Rails. See Curlybars::TemplateHandler for a typical use.
18
+ #
19
+ # Examples
20
+ #
21
+ # class BlogPresenter < Curlybars::Presenter
22
+ # presents :post
23
+ # allow_methods :title, :body, :author
24
+ #
25
+ # def title
26
+ # @post.title
27
+ # end
28
+ #
29
+ # def body
30
+ # markdown(@post.body)
31
+ # end
32
+ #
33
+ # def author
34
+ # @post.author.full_name
35
+ # end
36
+ # end
37
+ #
38
+ # presenter = BlogPresenter.new(context, post: post)
39
+ # presenter.author #=> "Jackie Chan"
40
+ #
41
+ class Presenter
42
+ extend Curlybars::MethodWhitelist
43
+
44
+ # Initializes the presenter with the given context and options.
45
+ #
46
+ # context - An ActionView::Base context.
47
+ # options - A Hash of options given to the presenter.
48
+ def initialize(context, options = {})
49
+ @_context = context
50
+ options.stringify_keys!
51
+
52
+ self.class.presented_names.each do |name|
53
+ value = options.fetch(name) do
54
+ default_values.fetch(name) do
55
+ block = default_blocks.fetch(name) do
56
+ raise ArgumentError.new("required identifier `#{name}` missing")
57
+ end
58
+
59
+ instance_exec(name, &block)
60
+ end
61
+ end
62
+
63
+ instance_variable_set("@#{name}", value)
64
+ end
65
+ end
66
+
67
+ # Sets up the view.
68
+ #
69
+ # Override this method in your presenter in order to do setup before the
70
+ # template is rendered. One use case is to call `content_for` in order
71
+ # to inject content into other templates, e.g. a layout.
72
+ #
73
+ # Examples
74
+ #
75
+ # class Posts::ShowPresenter < Curlybars::Presenter
76
+ # presents :post
77
+ #
78
+ # def setup!
79
+ # content_for :page_title, @post.title
80
+ # end
81
+ # end
82
+ #
83
+ # Returns nothing.
84
+ def setup!
85
+ # Does nothing.
86
+ end
87
+
88
+ # The key that should be used to cache the view.
89
+ #
90
+ # Unless `#cache_key` returns nil, the result of rendering the template
91
+ # that the presenter supports will be cached. The return value will be
92
+ # part of the final cache key, along with a digest of the template itself.
93
+ #
94
+ # Any object can be used as a cache key, so long as it
95
+ #
96
+ # - is a String,
97
+ # - responds to #cache_key itself, or
98
+ # - is an Array or a Hash whose items themselves fit either of these
99
+ # criteria.
100
+ #
101
+ # Returns the cache key Object or nil if no caching should be performed.
102
+ def cache_key
103
+ nil
104
+ end
105
+
106
+ # The options that should be passed to the cache backend when caching the
107
+ # view. The exact options may vary depending on the backend you're using.
108
+ #
109
+ # The most common option is `:expires_in`, which controls the duration of
110
+ # time that the cached view should be considered fresh. Because it's so
111
+ # common, you can set that option simply by defining `#cache_duration`.
112
+ #
113
+ # Note: if you set the `:expires_in` option through this method, the
114
+ # `#cache_duration` value will be ignored.
115
+ #
116
+ # Returns a Hash.
117
+ def cache_options
118
+ {}
119
+ end
120
+
121
+ # The duration that the view should be cached for. Only relevant if
122
+ # `#cache_key` returns a non nil value.
123
+ #
124
+ # If nil, the view will not have an expiration time set. See also
125
+ # `#cache_options` for a more flexible way to set cache options.
126
+ #
127
+ # Examples
128
+ #
129
+ # def cache_duration
130
+ # 10.minutes
131
+ # end
132
+ #
133
+ # Returns the Fixnum duration of the cache item, in seconds, or nil if no
134
+ # duration should be set.
135
+ def cache_duration
136
+ nil
137
+ end
138
+
139
+ class << self
140
+ # The name of the presenter class for a given view path.
141
+ #
142
+ # path - The String path of a view.
143
+ #
144
+ # Examples
145
+ #
146
+ # Curlybars::TemplateHandler.presenter_name_for_path("foo/bar")
147
+ # #=> "Foo::BarPresenter"
148
+ #
149
+ # Returns the String name of the matching presenter class.
150
+ def presenter_name_for_path(path)
151
+ "#{path}_presenter".camelize
152
+ end
153
+
154
+ # Returns the presenter class for the given path.
155
+ #
156
+ # path - The String path of a template.
157
+ #
158
+ # Returns the Class or nil if the constant cannot be found.
159
+ def presenter_for_path(path)
160
+ name_space = Curlybars.configuration.presenters_namespace
161
+ name_spaced_path = File.join(name_space, path)
162
+ full_class_name = presenter_name_for_path(name_spaced_path)
163
+ begin
164
+ full_class_name.constantize
165
+ rescue NameError
166
+ nil
167
+ end
168
+ end
169
+
170
+ # The set of view paths that the presenter depends on.
171
+ #
172
+ # Examples
173
+ #
174
+ # class Posts::ShowPresenter < Curlybars::Presenter
175
+ # version 2
176
+ # depends_on 'posts/comment', 'posts/comment_form'
177
+ # end
178
+ #
179
+ # Posts::ShowPresenter.dependencies
180
+ # #=> ['posts/comment', 'posts/comment_form']
181
+ #
182
+ # Returns a Set of String view paths.
183
+ def dependencies
184
+ # The base presenter doesn't have any dependencies.
185
+ return SortedSet.new if self == Curlybars::Presenter
186
+
187
+ @dependencies ||= SortedSet.new
188
+ @dependencies.union(superclass.dependencies)
189
+ end
190
+
191
+ # Indicate that the presenter depends a list of other views.
192
+ #
193
+ # deps - A list of String view paths that the presenter depends on.
194
+ #
195
+ # Returns nothing.
196
+ def depends_on(*dependencies)
197
+ @dependencies ||= SortedSet.new
198
+ @dependencies.merge(dependencies)
199
+ end
200
+
201
+ # Get or set the version of the presenter.
202
+ #
203
+ # version - The Integer version that should be set. If nil, no version
204
+ # is set.
205
+ #
206
+ # Returns the current Integer version of the presenter.
207
+ def version(version = nil)
208
+ @version = version if version.present?
209
+ @version || 0
210
+ end
211
+
212
+ # The cache key for the presenter class. Includes all dependencies as
213
+ # well.
214
+ #
215
+ # Returns a String cache key.
216
+ def cache_key
217
+ @cache_key ||= compute_cache_key
218
+ end
219
+
220
+ private
221
+
222
+ def compute_cache_key
223
+ dependency_cache_keys = dependencies.map do |path|
224
+ presenter = presenter_for_path(path)
225
+ if presenter.present?
226
+ presenter.cache_key
227
+ else
228
+ path
229
+ end
230
+ end
231
+
232
+ [name, version, dependency_cache_keys].flatten.join("/")
233
+ end
234
+
235
+ def presents(*args, **options, &block)
236
+ if options.key?(:default) && block_given?
237
+ raise ArgumentError, "Cannot provide both `default:` and block"
238
+ end
239
+
240
+ self.presented_names += args.map(&:to_s)
241
+
242
+ if options.key?(:default)
243
+ args.each do |arg|
244
+ self.default_values = default_values.merge(arg.to_s => options[:default]).freeze
245
+ end
246
+ end
247
+
248
+ return unless block_given?
249
+
250
+ args.each do |arg|
251
+ self.default_blocks = default_blocks.merge(arg.to_s => block).freeze
252
+ end
253
+ end
254
+
255
+ def exposes_helper(*methods)
256
+ methods.each do |method_name|
257
+ define_method(method_name) do |*args|
258
+ @_context.public_send(method_name, *args)
259
+ end
260
+ end
261
+ end
262
+
263
+ alias_method :exposes_helpers, :exposes_helper
264
+ end
265
+
266
+ private
267
+
268
+ class_attribute :presented_names, :default_values, :default_blocks
269
+
270
+ self.presented_names = [].freeze
271
+ self.default_values = {}.freeze
272
+ self.default_blocks = {}.freeze
273
+
274
+ delegate :render, to: :@_context
275
+
276
+ # Delegates private method calls to the current view context.
277
+ #
278
+ # The view context, an instance of ActionView::Base, is set by Rails.
279
+ def method_missing(method, *args, &block)
280
+ @_context.public_send(method, *args, &block)
281
+ end
282
+
283
+ # Tells ruby (and developers) what methods we can accept.
284
+ def respond_to_missing?(method, include_private = false)
285
+ @_context.respond_to?(method, false)
286
+ end
287
+ end
288
+ end