curlybars 0.9.13

Sign up to get free protection for your applications and to get access to all the features.
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