rjspotter-innate 2009.06.29

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 (128) hide show
  1. data/AUTHORS +10 -0
  2. data/CHANGELOG +3261 -0
  3. data/COPYING +18 -0
  4. data/MANIFEST +127 -0
  5. data/README.md +563 -0
  6. data/Rakefile +39 -0
  7. data/example/app/retro_games.rb +60 -0
  8. data/example/app/todo/layout/default.xhtml +11 -0
  9. data/example/app/todo/spec/todo.rb +63 -0
  10. data/example/app/todo/start.rb +51 -0
  11. data/example/app/todo/view/index.xhtml +39 -0
  12. data/example/app/whywiki_erb/layout/wiki.html.erb +15 -0
  13. data/example/app/whywiki_erb/spec/wiki.rb +19 -0
  14. data/example/app/whywiki_erb/start.rb +42 -0
  15. data/example/app/whywiki_erb/view/edit.erb +6 -0
  16. data/example/app/whywiki_erb/view/index.erb +12 -0
  17. data/example/custom_middleware.rb +35 -0
  18. data/example/hello.rb +11 -0
  19. data/example/howto_spec.rb +35 -0
  20. data/example/link.rb +27 -0
  21. data/example/provides.rb +31 -0
  22. data/example/session.rb +38 -0
  23. data/innate.gemspec +41 -0
  24. data/lib/innate.rb +269 -0
  25. data/lib/innate/action.rb +137 -0
  26. data/lib/innate/adapter.rb +76 -0
  27. data/lib/innate/cache.rb +134 -0
  28. data/lib/innate/cache/api.rb +128 -0
  29. data/lib/innate/cache/drb.rb +58 -0
  30. data/lib/innate/cache/file_based.rb +44 -0
  31. data/lib/innate/cache/marshal.rb +20 -0
  32. data/lib/innate/cache/memory.rb +21 -0
  33. data/lib/innate/cache/yaml.rb +20 -0
  34. data/lib/innate/current.rb +35 -0
  35. data/lib/innate/dynamap.rb +96 -0
  36. data/lib/innate/helper.rb +185 -0
  37. data/lib/innate/helper/aspect.rb +124 -0
  38. data/lib/innate/helper/cgi.rb +54 -0
  39. data/lib/innate/helper/flash.rb +36 -0
  40. data/lib/innate/helper/link.rb +94 -0
  41. data/lib/innate/helper/redirect.rb +85 -0
  42. data/lib/innate/helper/render.rb +152 -0
  43. data/lib/innate/helper/send_file.rb +26 -0
  44. data/lib/innate/log.rb +20 -0
  45. data/lib/innate/log/color_formatter.rb +49 -0
  46. data/lib/innate/log/hub.rb +77 -0
  47. data/lib/innate/middleware_compiler.rb +65 -0
  48. data/lib/innate/mock.rb +49 -0
  49. data/lib/innate/node.rb +1029 -0
  50. data/lib/innate/options.rb +37 -0
  51. data/lib/innate/options/dsl.rb +205 -0
  52. data/lib/innate/options/stub.rb +7 -0
  53. data/lib/innate/request.rb +141 -0
  54. data/lib/innate/response.rb +24 -0
  55. data/lib/innate/route.rb +114 -0
  56. data/lib/innate/session.rb +133 -0
  57. data/lib/innate/session/flash.rb +94 -0
  58. data/lib/innate/spec.rb +1 -0
  59. data/lib/innate/spec/bacon.rb +28 -0
  60. data/lib/innate/state.rb +26 -0
  61. data/lib/innate/state/accessor.rb +130 -0
  62. data/lib/innate/traited.rb +90 -0
  63. data/lib/innate/trinity.rb +18 -0
  64. data/lib/innate/version.rb +3 -0
  65. data/lib/innate/view.rb +97 -0
  66. data/lib/innate/view/erb.rb +14 -0
  67. data/lib/innate/view/etanni.rb +33 -0
  68. data/lib/innate/view/none.rb +9 -0
  69. data/spec/example/app/retro_games.rb +30 -0
  70. data/spec/example/hello.rb +13 -0
  71. data/spec/example/link.rb +25 -0
  72. data/spec/example/provides.rb +16 -0
  73. data/spec/example/session.rb +22 -0
  74. data/spec/helper.rb +10 -0
  75. data/spec/innate/action/layout.rb +121 -0
  76. data/spec/innate/action/layout/file_layout.xhtml +1 -0
  77. data/spec/innate/cache/common.rb +47 -0
  78. data/spec/innate/cache/marshal.rb +5 -0
  79. data/spec/innate/cache/memory.rb +5 -0
  80. data/spec/innate/cache/yaml.rb +5 -0
  81. data/spec/innate/dynamap.rb +22 -0
  82. data/spec/innate/helper.rb +86 -0
  83. data/spec/innate/helper/aspect.rb +75 -0
  84. data/spec/innate/helper/cgi.rb +37 -0
  85. data/spec/innate/helper/flash.rb +115 -0
  86. data/spec/innate/helper/link.rb +139 -0
  87. data/spec/innate/helper/redirect.rb +171 -0
  88. data/spec/innate/helper/render.rb +165 -0
  89. data/spec/innate/helper/send_file.rb +21 -0
  90. data/spec/innate/helper/view/aspect_hello.xhtml +1 -0
  91. data/spec/innate/helper/view/locals.xhtml +1 -0
  92. data/spec/innate/helper/view/loop.xhtml +4 -0
  93. data/spec/innate/helper/view/num.xhtml +1 -0
  94. data/spec/innate/helper/view/partial.xhtml +1 -0
  95. data/spec/innate/helper/view/recursive.xhtml +7 -0
  96. data/spec/innate/mock.rb +84 -0
  97. data/spec/innate/modes.rb +61 -0
  98. data/spec/innate/node/mapping.rb +37 -0
  99. data/spec/innate/node/node.rb +135 -0
  100. data/spec/innate/node/resolve.rb +82 -0
  101. data/spec/innate/node/view/another_layout/another_layout.xhtml +3 -0
  102. data/spec/innate/node/view/bar.xhtml +1 -0
  103. data/spec/innate/node/view/foo.html.xhtml +1 -0
  104. data/spec/innate/node/view/only_view.xhtml +1 -0
  105. data/spec/innate/node/view/with_layout.xhtml +1 -0
  106. data/spec/innate/node/wrap_action_call.rb +83 -0
  107. data/spec/innate/options.rb +123 -0
  108. data/spec/innate/parameter.rb +154 -0
  109. data/spec/innate/provides.rb +99 -0
  110. data/spec/innate/provides/list.html.xhtml +1 -0
  111. data/spec/innate/provides/list.txt.xhtml +1 -0
  112. data/spec/innate/request.rb +79 -0
  113. data/spec/innate/route.rb +135 -0
  114. data/spec/innate/session.rb +58 -0
  115. data/spec/innate/traited.rb +55 -0
  116. data/tasks/authors.rake +30 -0
  117. data/tasks/bacon.rake +66 -0
  118. data/tasks/changelog.rake +18 -0
  119. data/tasks/gem.rake +22 -0
  120. data/tasks/gem_setup.rake +99 -0
  121. data/tasks/grancher.rake +12 -0
  122. data/tasks/manifest.rake +4 -0
  123. data/tasks/rcov.rake +19 -0
  124. data/tasks/release.rake +53 -0
  125. data/tasks/reversion.rake +8 -0
  126. data/tasks/setup.rake +6 -0
  127. data/tasks/ycov.rake +84 -0
  128. metadata +218 -0
@@ -0,0 +1,20 @@
1
+ require 'innate/log/hub'
2
+ require 'innate/log/color_formatter'
3
+
4
+ module Innate
5
+ logdev = $stderr
6
+ logger = Logger.new(logdev)
7
+
8
+ if Logger::ColorFormatter.color?(logdev)
9
+ begin
10
+ require 'win32console' if RUBY_PLATFORM =~ /win32/i
11
+
12
+ logger.formatter = Logger::ColorFormatter.new
13
+
14
+ rescue LoadError
15
+ logger.debug "For colors on windows, please `gem install win32console`"
16
+ end
17
+ end
18
+
19
+ Log = LogHub.new(logger)
20
+ end
@@ -0,0 +1,49 @@
1
+ class Logger
2
+ # Extended Formatter that supports ANSI colors.
3
+ #
4
+ # The basic mapping of ANSI colors is as follows:
5
+ #
6
+ # | reset | bold | dark | underline | blink | negative
7
+ # MOD | 0 | 1 | 2 | 4 | 5 | 7
8
+ #
9
+ # | black | red | green | yellow | blue | magenta | cyan | white
10
+ # FG | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37
11
+ # BG | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47
12
+ #
13
+ # The output is done by: "\e[#{mod};#{fg};#{bg}m#{string}\e[0m"
14
+ # The suffix is to reset the terminal to the original state again.
15
+ class ColorFormatter < Formatter
16
+ LEVEL_COLOR = {
17
+ 'DEBUG' => "\e[0;34;40m%s\e[0m", # blue on black
18
+ 'INFO' => "\e[0;37;40m%s\e[0m", # white on black
19
+ 'WARN' => "\e[0;33;40m%s\e[0m", # yellow on black
20
+ 'ERROR' => "\e[0;31;40m%s\e[0m", # red on black
21
+ 'FATAL' => "\e[0;35;40m%s\e[0m", # red on black
22
+ 'UNKNOWN' => "\e[0;32;40m%s\e[0m", # green on black
23
+ }
24
+
25
+ FORMAT_TIME = "%Y-%m-%d %H:%M:%S"
26
+ FORMAT_LINE = "%s [%s $%d] %5s | %s: %s\n"
27
+
28
+ def call(severity, time, program, message)
29
+ hint = severity[0,1]
30
+ time = format_time(time)
31
+ pid = $$
32
+ string = colorize(msg2str(message), severity)
33
+
34
+ FORMAT_LINE % [hint, time, pid, severity, program, string]
35
+ end
36
+
37
+ def format_time(time)
38
+ time.strftime(FORMAT_TIME)
39
+ end
40
+
41
+ def colorize(string, severity)
42
+ LEVEL_COLOR[severity] % string
43
+ end
44
+
45
+ def self.color?(logdev)
46
+ logdev.respond_to?(:tty?) and logdev.tty?
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,77 @@
1
+ module Innate
2
+ # Innate only provides logging via stdlib Logger to avoid bloat and
3
+ # dependencies, you may specify multiple loggers in the Log instance of LogHub
4
+ # to accomendate your needs, by default we log to $stderr to be compatible with
5
+ # CGI.
6
+ #
7
+ # Please read the documentation of logger.rb (or even better, its source) to
8
+ # get a feeling of how to use it correctly within Innate
9
+ #
10
+ # A few shortcuts:
11
+ #
12
+ # 1. Create logger for stderr/stdout
13
+ # logger = Logger.new($stdout)
14
+ # logger = Logger.new($stderr)
15
+ #
16
+ # 2. Create logger for a file
17
+ #
18
+ # logger = Logger.new('test.log')
19
+ #
20
+ # 3. Create logger for file object
21
+ #
22
+ # file = File.open('test.log', 'a+')
23
+ # logger = Logger.new(file)
24
+ #
25
+ # 4. Create logger with rotation on specified file size
26
+ #
27
+ # # 10 files history, 5 MB each
28
+ # logger = Logger.new('test.log', 10, (5 << 20))
29
+ #
30
+ # # 100 files history, 1 MB each
31
+ # logger = Logger.new('test.log', 100, (1 << 20))
32
+ #
33
+ # 5. Create a logger which ages logfiles daily/weekly/monthly
34
+ #
35
+ # logger = Logger.new('test.log', 'daily')
36
+ # logger = Logger.new('test.log', 'weekly')
37
+ # logger = Logger.new('test.log', 'monthly')
38
+
39
+ class LogHub
40
+ include Logger::Severity
41
+ include Optioned
42
+
43
+ attr_accessor :loggers, :program, :active
44
+
45
+ # +loggers+ should be a list of Logger instances
46
+ def initialize(*loggers)
47
+ @loggers = loggers.flatten
48
+ @program = nil
49
+ @active = true
50
+ self.level = DEBUG
51
+ end
52
+
53
+ # set level for all loggers
54
+ def level=(lvl)
55
+ @loggers.each{|l| l.level = lvl }
56
+ @level = lvl
57
+ end
58
+
59
+ def start; @active = true; end
60
+ def stop; @active = false; end
61
+
62
+ def method_missing(meth, *args, &block)
63
+ eval %~
64
+ def #{meth}(*args, &block)
65
+ return unless @active
66
+ args.each{|arg| @loggers.each{|logger| logger.#{meth}(arg, &block) }}
67
+ end
68
+ ~
69
+
70
+ send(meth, *args, &block)
71
+ end
72
+
73
+ def write(*args)
74
+ self.<<(*args)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ module Innate
2
+ class MiddlewareCompiler
3
+ COMPILED = {}
4
+
5
+ def self.build(name, &block)
6
+ COMPILED[name] ||= new(name, &block)
7
+ end
8
+
9
+ def self.build!(name, &block)
10
+ COMPILED[name] = new(name, &block)
11
+ end
12
+
13
+ attr_reader :middlewares, :name
14
+
15
+ def initialize(name)
16
+ @name = name.to_sym
17
+ @middlewares = []
18
+ @compiled = nil
19
+ yield(self) if block_given?
20
+ end
21
+
22
+ def use(app, *args, &block)
23
+ @middlewares << [app, args, block]
24
+ end
25
+
26
+ def apps(*middlewares)
27
+ @middlewares.concat(middlewares.map{|mw| [mw, [], nil]})
28
+ end
29
+
30
+ def run(app)
31
+ @app = app
32
+ end
33
+
34
+ def cascade(*apps)
35
+ @app = Rack::Cascade.new(apps)
36
+ end
37
+
38
+ # Default application for Innate
39
+ def innate(app = Innate::DynaMap, options = Innate.options)
40
+ roots, publics = options[:roots], options[:publics]
41
+
42
+ joined = roots.map{|root| publics.map{|public| ::File.join(root, public)}}
43
+
44
+ apps = joined.flatten.map{|pr| Rack::File.new(pr) }
45
+ apps << Current.new(Route.new(app), Rewrite.new(app))
46
+
47
+ cascade(*apps)
48
+ end
49
+
50
+ def call(env)
51
+ compile
52
+ @compiled.call(env)
53
+ end
54
+
55
+ def compile
56
+ @compiled ? self : compile!
57
+ end
58
+
59
+ def compile!
60
+ @compiled = @middlewares.reverse.inject(@app){|s, (app, args, block)|
61
+ app.new(s, *args, &block) }
62
+ self
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,49 @@
1
+ module Innate
2
+ module Mock
3
+ HTTP_METHODS = %w[ CONNECT DELETE GET HEAD OPTIONS POST PUT TRACE ]
4
+ OPTIONS = {:app => Innate}
5
+
6
+ HTTP_METHODS.each do |method|
7
+ (class << self; self; end).
8
+ send(:define_method, method.downcase){|*args|
9
+ mock(method, *args)
10
+ }
11
+ end
12
+
13
+ def self.mock(method, *args)
14
+ mock_request.request(method, *args)
15
+ end
16
+
17
+ def self.mock_request(app = OPTIONS[:app])
18
+ Rack::MockRequest.new(app)
19
+ end
20
+
21
+ def self.session
22
+ yield Session.new
23
+ end
24
+
25
+ class Session
26
+ attr_accessor :cookie
27
+
28
+ def initialize
29
+ @cookie = nil
30
+ end
31
+
32
+ HTTP_METHODS.each do |method|
33
+ define_method(method.downcase){|*args|
34
+ extract_cookie(method, *args)
35
+ }
36
+ end
37
+
38
+ def extract_cookie(method, path, hash = {})
39
+ hash['HTTP_COOKIE'] ||= @cookie if @cookie
40
+ response = Mock::mock(method, path, hash)
41
+
42
+ cookie = response['Set-Cookie']
43
+ @cookie = cookie if cookie
44
+
45
+ response
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,1029 @@
1
+ module Innate
2
+
3
+ # The nervous system of {Innate}, so you can relax.
4
+ #
5
+ # Node may be included into any class to make it a valid responder to
6
+ # requests.
7
+ #
8
+ # The major difference between this and the old Ramaze controller is that
9
+ # every Node acts as a standalone application with its own dispatcher.
10
+ #
11
+ # What's also an important difference is the fact that {Node} is a module, so
12
+ # we don't have to spend a lot of time designing the perfect subclassing
13
+ # scheme.
14
+ #
15
+ # This makes dispatching more fun, avoids a lot of processing that is done by
16
+ # {Rack} anyway and lets you tailor your application down to the last action
17
+ # exactly the way you want without worrying about side-effects to other
18
+ # {Node}s.
19
+ #
20
+ # Upon inclusion, it will also include {Innate::Trinity} and {Innate::Helper}
21
+ # to provide you with {Innate::Request}, {Innate::Response},
22
+ # {Innate::Session} instances, and all the standard helper methods as well as
23
+ # the ability to simply add other helpers.
24
+ #
25
+ # Please note that method_missing will _not_ be considered when building an
26
+ # {Action}. There might be future demand for this, but for now you can simply
27
+ # use `def index(*args); end` to make a catch-all action.
28
+ module Node
29
+ include Traited
30
+
31
+ attr_reader :method_arities, :layout_templates, :view_templates
32
+
33
+ NODE_LIST = Set.new
34
+
35
+ # These traits are inherited into ancestors, changing a trait in an
36
+ # ancestor doesn't affect the higher ones.
37
+ #
38
+ # class Foo; include Innate::Node; end
39
+ # class Bar < Foo; end
40
+ #
41
+ # Foo.trait[:wrap] == Bar.trait[:wrap] # => true
42
+ # Bar.trait(:wrap => [:cache_wrap])
43
+ # Foo.trait[:wrap] == Bar.trait[:wrap] # => false
44
+
45
+ trait :views => []
46
+ trait :layouts => []
47
+ trait :layout => nil
48
+ trait :alias_view => {}
49
+ trait :provide => {}
50
+
51
+ # @see wrap_action_call
52
+ trait :wrap => SortedSet.new
53
+ trait :provide_set => false
54
+ trait :needs_method => false
55
+ trait :skip_node_map => false
56
+
57
+ # Upon inclusion we make ourselves comfortable.
58
+ def self.included(into)
59
+ into.__send__(:include, Helper)
60
+ into.extend(Trinity)
61
+ into.extend(self)
62
+
63
+ NODE_LIST << into
64
+
65
+ return if into.provide_set?
66
+ into.provide(:html, :engine => :Etanni)
67
+ into.trait(:provide_set => false)
68
+ end
69
+
70
+ # node mapping procedure
71
+ #
72
+ # when Node is included into an object, it's added to NODE_LIST
73
+ # when object::map(location) is sent, it maps the object into DynaMap
74
+ # when Innate.start is issued, it calls Node::setup
75
+ # Node::setup iterates NODE_LIST and maps all objects not in DynaMap by
76
+ # using Node::generate_mapping(object.name) as location
77
+ #
78
+ # when object::map(nil) is sent, the object will be skipped in Node::setup
79
+
80
+ def self.setup
81
+ NODE_LIST.each{|node|
82
+ node.map(generate_mapping(node.name)) unless node.trait[:skip_node_map]
83
+ }
84
+ # Log.debug("Mapped Nodes: %p" % DynaMap.to_hash) unless NODE_LIST.empty?
85
+ end
86
+
87
+ def self.generate_mapping(object_name = self.name)
88
+ return '/' if NODE_LIST.size == 1
89
+ parts = object_name.split('::').map{|part|
90
+ part.gsub(/^[A-Z]+/){|sub| sub.downcase }.gsub(/[A-Z]+[^A-Z]/, '_\&')
91
+ }
92
+ '/' << parts.join('/').downcase
93
+ end
94
+
95
+ # Tries to find the relative url that this {Node} is mapped to.
96
+ # If it cannot find one it will instead generate one based on the
97
+ # snake_cased name of itself.
98
+ #
99
+ # @example Usage:
100
+ #
101
+ # class FooBar
102
+ # include Innate::Node
103
+ # end
104
+ # FooBar.mapping # => '/foo_bar'
105
+ #
106
+ # @return [String] the relative path to the node
107
+ #
108
+ # @api external
109
+ # @see Innate::SingletonMethods#to
110
+ # @author manveru
111
+ def mapping
112
+ Innate.to(self)
113
+ end
114
+
115
+ # Shortcut to map or remap this Node.
116
+ #
117
+ # @example Usage for explicit mapping:
118
+ #
119
+ # class FooBar
120
+ # include Innate::Node
121
+ # map '/foo_bar'
122
+ # end
123
+ #
124
+ # Innate.to(FooBar) # => '/foo_bar'
125
+ #
126
+ # @example Usage for automatic mapping:
127
+ #
128
+ # class FooBar
129
+ # include Innate::Node
130
+ # map mapping
131
+ # end
132
+ #
133
+ # Innate.to(FooBar) # => '/foo_bar'
134
+ #
135
+ # @param [#to_s] location
136
+ #
137
+ # @api external
138
+ # @see Innate::SingletonMethods::map
139
+ # @author manveru
140
+ def map(location)
141
+ trait :skip_node_map => true
142
+ Innate.map(location, self) if location
143
+ end
144
+
145
+ # Specify which way contents are provided and processed.
146
+ #
147
+ # Use this to set a templating engine, custom Content-Type, or pass a block
148
+ # to take over the processing of the {Action} and template yourself.
149
+ #
150
+ # Provides set via this method will be inherited into subclasses.
151
+ #
152
+ # The +format+ is extracted from the PATH_INFO, it simply represents the
153
+ # last extension name in the path.
154
+ #
155
+ # The provide also has influence on the chosen templates for the {Action}.
156
+ #
157
+ # @example providing RSS with ERB templating
158
+ #
159
+ # provide :rss, :engine => :ERB
160
+ #
161
+ # Given a request to `/list.rss` the template lookup first tries to find
162
+ # `list.rss.erb`, if that fails it falls back to `list.erb`.
163
+ # If neither of these are available it will try to use the return value of
164
+ # the method in the {Action} as template.
165
+ #
166
+ # A request to `/list.yaml` would match the format 'yaml'
167
+ #
168
+ # @example providing a yaml version of actions
169
+ #
170
+ # class Articles
171
+ # include Innate::Node
172
+ # map '/article'
173
+ #
174
+ # provide(:yaml, :type => 'text/yaml'){|action, value| value.to_yaml }
175
+ #
176
+ # def list
177
+ # @articles = Article.list
178
+ # end
179
+ # end
180
+ #
181
+ # @example providing plain text inspect version
182
+ #
183
+ # class Articles
184
+ # include Innate::Node
185
+ # map '/article'
186
+ #
187
+ # provide(:txt, :type => 'text/plain'){|action, value| value.inspect }
188
+ #
189
+ # def list
190
+ # @articles = Article.list
191
+ # end
192
+ # end
193
+ #
194
+ # @param [Proc] block
195
+ # upon calling the action, [action, value] will be passed to it and its
196
+ # return value becomes the response body.
197
+ #
198
+ # @option param :engine [Symbol String]
199
+ # Name of an engine for View::get
200
+ # @option param :type [String]
201
+ # default Content-Type if none was set in Response
202
+ #
203
+ # @raise [ArgumentError] if neither a block nor an engine was given
204
+ #
205
+ # @api external
206
+ # @see View::get Node#provides
207
+ # @author manveru
208
+ #
209
+ # @todo
210
+ # The comment of this method may be too short for the effects it has on
211
+ # the rest of Innate, if you feel something is missing please let me
212
+ # know.
213
+
214
+ def provide(format, param = {}, &block)
215
+ if param.respond_to?(:to_hash)
216
+ param = param.to_hash
217
+ handler = block || View.get(param[:engine])
218
+ content_type = param[:type]
219
+ else
220
+ handler = View.get(param)
221
+ end
222
+
223
+ raise(ArgumentError, "Need an engine or block") unless handler
224
+
225
+ trait("#{format}_handler" => handler, :provide_set => true)
226
+ trait("#{format}_content_type" => content_type) if content_type
227
+ end
228
+
229
+ def provides
230
+ ancestral_trait.reject{|k,v| k !~ /_handler$/ }
231
+ end
232
+
233
+ # This makes the Node a valid application for Rack.
234
+ # +env+ is the environment hash passed from the Rack::Handler
235
+ #
236
+ # We rely on correct PATH_INFO.
237
+ #
238
+ # As defined by the Rack spec, PATH_INFO may be empty if it wants the root
239
+ # of the application, so we insert '/' to make our dispatcher simple.
240
+ #
241
+ # Innate will not rescue any errors for you or do any error handling, this
242
+ # should be done by an underlying middleware.
243
+ #
244
+ # We do however log errors at some vital points in order to provide you
245
+ # with feedback in your logs.
246
+ #
247
+ # A lot of functionality in here relies on the fact that call is executed
248
+ # within Current#call which populates the variables used by Trinity.
249
+ # So if you use the Node directly as a middleware make sure that you #use
250
+ # Innate::Current as a middleware before it.
251
+ #
252
+ # @param [Hash] env
253
+ #
254
+ # @return [Array]
255
+ #
256
+ # @api external
257
+ # @see Response#reset Node#try_resolve Session#flush
258
+ # @author manveru
259
+
260
+ def call(env)
261
+ path = env['PATH_INFO']
262
+ path << '/' if path.empty?
263
+
264
+ response.reset
265
+ try_resolve(path).finish
266
+ end
267
+
268
+ # Let's try to find some valid action for given +path+.
269
+ # Otherwise we dispatch to {action_missing}.
270
+ #
271
+ # @param [String] path from env['PATH_INFO']
272
+ #
273
+ # @return [Response]
274
+ #
275
+ # @api external
276
+ # @see Node#resolve Node#action_found Node#action_missing
277
+ # @author manveru
278
+ def try_resolve(path)
279
+ action = resolve(path)
280
+ action ? action_found(action) : action_missing(path)
281
+ end
282
+
283
+ # Executed once an {Action} has been found.
284
+ #
285
+ # Reset the {Innate::Response} instance, catch :respond and :redirect.
286
+ # {Action#call} has to return a String.
287
+ #
288
+ # @param [Action] action
289
+ #
290
+ # @return [Innate::Response]
291
+ #
292
+ # @api external
293
+ # @see Action#call Innate::Response
294
+ # @author manveru
295
+ def action_found(action)
296
+ response = catch(:respond){ catch(:redirect){ action.call }}
297
+
298
+ unless response.respond_to?(:finish)
299
+ self.response.write(response)
300
+ response = self.response
301
+ end
302
+
303
+ response['Content-Type'] ||= action.options[:content_type]
304
+ response
305
+ end
306
+
307
+ # The default handler in case no action was found, kind of method_missing.
308
+ # Must modify the response in order to have any lasting effect.
309
+ #
310
+ # Reasoning:
311
+ # * We are doing this is in order to avoid tons of special error handling
312
+ # code that would impact runtime and make the overall API more
313
+ # complicated.
314
+ # * This cannot be a normal action is that methods defined in
315
+ # {Innate::Node} will never be considered for actions.
316
+ #
317
+ # To use a normal action with template do following:
318
+ #
319
+ # @example
320
+ #
321
+ # class Hi
322
+ # include Innate::Node
323
+ # map '/'
324
+ #
325
+ # def self.action_missing(path)
326
+ # return if path == '/not_found'
327
+ # # No normal action, runs on bare metal
328
+ # try_resolve('/not_found')
329
+ # end
330
+ #
331
+ # def not_found
332
+ # # Normal action
333
+ # "Sorry, I do not exist"
334
+ # end
335
+ # end
336
+ #
337
+ # @param [String] path
338
+ #
339
+ # @api external
340
+ # @see Innate::Response Node#try_resolve
341
+ # @author manveru
342
+ def action_missing(path)
343
+ response.status = 404
344
+ response['Content-Type'] = 'text/plain'
345
+ response.write("No action found at: %p" % path)
346
+
347
+ response
348
+ end
349
+
350
+ # Let's get down to business, first check if we got any wishes regarding
351
+ # the representation from the client, otherwise we will assume he wants
352
+ # html.
353
+ #
354
+ # @param [String] path
355
+ #
356
+ # @return [nil, Action]
357
+ #
358
+ # @api external
359
+ # @see Node::find_provide Node::update_method_arities Node::find_action
360
+ # @author manveru
361
+ def resolve(path)
362
+ name, wish, engine = find_provide(path)
363
+ node = (respond_to?(:ancestors) && respond_to?(:new)) ? self : self.class
364
+ action = Action.create(:node => node, :wish => wish, :engine => engine, :path => path)
365
+
366
+ if content_type = node.ancestral_trait["#{wish}_content_type"]
367
+ action.options = {:content_type => content_type}
368
+ end
369
+
370
+ node.update_method_arities
371
+ node.update_template_mappings
372
+ node.fill_action(action, name)
373
+ end
374
+
375
+ # Resolve possible provides for the given +path+ from {provides}.
376
+ #
377
+ # @param [String] path
378
+ #
379
+ # @return [Array] with name, wish, engine
380
+ #
381
+ # @api internal
382
+ # @see Node::provide Node::provides
383
+ # @author manveru
384
+ def find_provide(path)
385
+ pr = provides
386
+
387
+ name, wish, engine = path, 'html', pr['html_handler']
388
+
389
+ pr.find do |key, value|
390
+ key = key[/(.*)_handler$/, 1]
391
+ next unless path =~ /^(.+)\.#{key}$/i
392
+ name, wish, engine = $1, key, value
393
+ end
394
+
395
+ return name, wish, engine
396
+ end
397
+
398
+ # Now we're talking {Action}, we try to find a matching template and
399
+ # method, if we can't find either we go to the next pattern, otherwise we
400
+ # answer with an {Action} with everything we know so far about the demands
401
+ # of the client.
402
+ #
403
+ # @param [String] given_name the name extracted from REQUEST_PATH
404
+ # @param [String] wish
405
+ #
406
+ # @return [Action, nil]
407
+ #
408
+ # @api internal
409
+ # @see Node#find_method Node#find_view Node#find_layout Node#patterns_for
410
+ # Action#wish Action#merge!
411
+ # @author manveru
412
+ def fill_action(action, given_name)
413
+ needs_method = self.needs_method?
414
+ wish = action.wish
415
+
416
+ patterns_for(given_name) do |name, params|
417
+ method = find_method(name, params)
418
+
419
+ next unless method if needs_method
420
+ next unless method if params.any?
421
+ next unless (view = find_view(name, wish)) or method
422
+
423
+ params.map!{|param| Rack::Utils.unescape(param) }
424
+
425
+ action.merge!(:method => method, :view => view, :params => params,
426
+ :layout => find_layout(name, wish))
427
+ end
428
+ end
429
+
430
+ # Try to find a suitable value for the layout. This may be a template or
431
+ # the name of a method.
432
+ #
433
+ # If a layout could be found, an Array with two elements is returned, the
434
+ # first indicating the kind of layout (:layout|:view|:method), the second
435
+ # the found value, which may be a String or Symbol.
436
+ #
437
+ # @param [String] name
438
+ # @param [String] wish
439
+ #
440
+ # @return [Array, nil]
441
+ #
442
+ # @api external
443
+ # @see Node#to_layout Node#find_method Node#find_view
444
+ # @author manveru
445
+ #
446
+ # @todo allow layouts combined of method and view... hairy :)
447
+ def find_layout(name, wish)
448
+ return unless layout = ancestral_trait[:layout]
449
+ return unless layout = layout.call(name, wish) if layout.respond_to?(:call)
450
+
451
+ if found = to_layout(layout, wish)
452
+ [:layout, found]
453
+ elsif found = find_view(layout, wish)
454
+ [:view, found]
455
+ elsif found = find_method(layout, [])
456
+ [:method, found]
457
+ end
458
+ end
459
+
460
+ # We check arity if possible, but will happily dispatch to any method that
461
+ # has default parameters.
462
+ # If you don't want your method to be responsible for messing up a request
463
+ # you should think twice about the arguments you specify due to limitations
464
+ # in Ruby.
465
+ #
466
+ # So if you want your method to take only one parameter which may have a
467
+ # default value following will work fine:
468
+ #
469
+ # def index(foo = "bar", *rest)
470
+ #
471
+ # But following will respond to /arg1/arg2 and then fail due to ArgumentError:
472
+ #
473
+ # def index(foo = "bar")
474
+ #
475
+ # Here a glance at how parameters are expressed in arity:
476
+ #
477
+ # def index(a) # => 1
478
+ # def index(a = :a) # => -1
479
+ # def index(a, *r) # => -2
480
+ # def index(a = :a, *r) # => -1
481
+ #
482
+ # def index(a, b) # => 2
483
+ # def index(a, b, *r) # => -3
484
+ # def index(a, b = :b) # => -2
485
+ # def index(a, b = :b, *r) # => -2
486
+ #
487
+ # def index(a = :a, b = :b) # => -1
488
+ # def index(a = :a, b = :b, *r) # => -1
489
+ #
490
+ # @param [String, Symbol] name
491
+ # @param [Array] params
492
+ #
493
+ # @return [String, Symbol]
494
+ #
495
+ # @api external
496
+ # @see Node#fill_action Node#find_layout
497
+ # @author manveru
498
+ #
499
+ # @todo Once 1.9 is mainstream we can use Method#parameters to do accurate
500
+ # prediction
501
+ def find_method(name, params)
502
+ return unless arity = method_arities[name]
503
+ name if arity == params.size or arity < 0
504
+ end
505
+
506
+ # Answer with a hash, keys are method names, values are method arities.
507
+ #
508
+ # Note that this will be executed once for every request, once we have
509
+ # settled things down a bit more we can switch to update based on Reloader
510
+ # hooks and update once on startup.
511
+ # However, that may cause problems with dynamically created methods, so
512
+ # let's play it safe for now.
513
+ #
514
+ # @example
515
+ #
516
+ # Hi.update_method_arities
517
+ # # => {'index' => 0, 'foo' => -1, 'bar => 2}
518
+ #
519
+ # @api internal
520
+ # @see Node#resolve
521
+ # @return [Hash] mapping the name of the methods to their arity
522
+ def update_method_arities
523
+ @method_arities = {}
524
+
525
+ exposed = ancestors & Helper::EXPOSE.to_a
526
+ higher = ancestors.select{|a| a < Innate::Node }
527
+
528
+ (higher + exposed).reverse_each do |ancestor|
529
+ ancestor.public_instance_methods(false).each do |im|
530
+ @method_arities[im.to_s] = ancestor.instance_method(im).arity
531
+ end
532
+ end
533
+
534
+ @method_arities
535
+ end
536
+
537
+ # Try to find the best template for the given basename and wish and respect
538
+ # aliased views.
539
+ #
540
+ # @param [#to_s] action_name
541
+ # @param [#to_s] wish
542
+ #
543
+ # @return [String, nil] depending whether a template could be found
544
+ #
545
+ # @api external
546
+ # @see Node#to_template Node#find_aliased_view
547
+ # @author manveru
548
+ def find_view(action_name, wish)
549
+ aliased = find_aliased_view(action_name, wish)
550
+ return aliased if aliased
551
+
552
+ to_view(action_name, wish)
553
+ end
554
+
555
+ # Try to find the best template for the given basename and wish.
556
+ #
557
+ # This method is mostly here for symetry with {to_layout} and to allow you
558
+ # overriding the template lookup easily.
559
+ #
560
+ # @param [#to_s] action_name
561
+ # @param [#to_s] wish
562
+ #
563
+ # @return [String, nil] depending whether a template could be found
564
+ #
565
+ # @api external
566
+ # @see {Node#find_view} {Node#to_template} {Node#root_mappings}
567
+ # {Node#view_mappings} {Node#to_template}
568
+ # @author manveru
569
+ def to_view(action_name, wish)
570
+ return unless files = view_templates[wish.to_s]
571
+ files[action_name.to_s]
572
+ end
573
+
574
+ # Aliasing one view from another.
575
+ # The aliases are inherited, and the optional third +node+ parameter
576
+ # indicates the Node to take the view from.
577
+ #
578
+ # The argument order is identical with `alias` and `alias_method`, which
579
+ # quite honestly confuses me, but at least we stay consistent.
580
+ #
581
+ # @example
582
+ # class Foo
583
+ # include Innate::Node
584
+ #
585
+ # # Use the 'foo' view when calling 'bar'
586
+ # alias_view 'bar', 'foo'
587
+ #
588
+ # # Use the 'foo' view from FooBar node when calling 'bar'
589
+ # alias_view 'bar', 'foo', FooBar
590
+ # end
591
+ #
592
+ # Note that the parameters have been simplified in comparision with
593
+ # Ramaze::Controller::template where the second parameter may be a
594
+ # Controller or the name of the template. We take that now as an optional
595
+ # third parameter.
596
+ #
597
+ # @param [#to_s] to view that should be replaced
598
+ # @param [#to_s] from view to use or Node.
599
+ # @param [#nil?, Node] node optionally obtain view from this Node
600
+ #
601
+ # @api external
602
+ # @see Node::find_aliased_view
603
+ # @author manveru
604
+ def alias_view(to, from, node = nil)
605
+ trait[:alias_view] || trait(:alias_view => {})
606
+ trait[:alias_view][to.to_s] = node ? [from.to_s, node] : from.to_s
607
+ end
608
+
609
+ # Resolve one level of aliasing for the given +action_name+ and +wish+.
610
+ #
611
+ # @param [String] action_name
612
+ # @param [String] wish
613
+ #
614
+ # @return [nil, String] the absolute path to the aliased template or nil
615
+ #
616
+ # @api internal
617
+ # @see Node::alias_view Node::find_view
618
+ # @author manveru
619
+ def find_aliased_view(action_name, wish)
620
+ aliased_name, aliased_node = ancestral_trait[:alias_view][action_name]
621
+ return unless aliased_name
622
+
623
+ aliased_node ||= self
624
+ aliased_node.update_view_mappings
625
+ aliased_node.find_view(aliased_name, wish)
626
+ end
627
+
628
+ # Find the best matching action_name for the layout, if any.
629
+ #
630
+ # This is mostly an abstract method that you might find handy if you want
631
+ # to do vastly different layout lookup.
632
+ #
633
+ # @param [String] action_name
634
+ # @param [String] wish
635
+ #
636
+ # @return [nil, String] the absolute path to the template or nil
637
+ #
638
+ # @api external
639
+ # @see {Node#to_template} {Node#root_mappings} {Node#layout_mappings}
640
+ # @author manveru
641
+ def to_layout(action_name, wish)
642
+ return unless files = layout_templates[wish.to_s]
643
+ files[action_name.to_s]
644
+ end
645
+
646
+ # Define a layout to use on this Node.
647
+ #
648
+ # A Node can only have one layout, although the template being chosen can
649
+ # depend on {provides}.
650
+ #
651
+ # @param [String, #to_s] name basename without extension of the layout to use
652
+ # @param [Proc, #call] block called on every dispatch if no name given
653
+ #
654
+ # @return [Proc, String] The assigned name or block
655
+ #
656
+ # @api external
657
+ # @see Node#find_layout Node#layout_paths Node#to_layout Node#app_layout
658
+ # @author manveru
659
+ #
660
+ # NOTE:
661
+ # The behaviour of Node#layout changed significantly from Ramaze, instead
662
+ # of multitudes of obscure options and methods like deny_layout we simply
663
+ # take a block and use the returned value as the name for the layout. No
664
+ # layout will be used if the block returns nil.
665
+ def layout(name = nil, &block)
666
+ if name and block
667
+ # default name, but still check with block
668
+ trait(:layout => lambda{|n, w| name if block.call(n, w) })
669
+ elsif name
670
+ # name of a method or template
671
+ trait(:layout => name.to_s)
672
+ elsif block
673
+ # call block every request with name and wish, returned value is name
674
+ # of layout template or method
675
+ trait(:layout => block)
676
+ else
677
+ # remove layout for this node
678
+ trait(:layout => nil)
679
+ end
680
+
681
+ return ancestral_trait[:layout]
682
+ end
683
+
684
+ # The innate beauty in Nitro, Ramaze, and {Innate}.
685
+ #
686
+ # Will yield the name of the action and parameter for the action method in
687
+ # order of significance.
688
+ #
689
+ # def foo__bar # responds to /foo/bar
690
+ # def foo(bar) # also responds to /foo/bar
691
+ #
692
+ # But foo__bar takes precedence because it's more explicit.
693
+ #
694
+ # The last fallback will always be the index action with all of the path
695
+ # turned into parameters.
696
+ #
697
+ # @example yielding possible combinations of action names and params
698
+ #
699
+ # class Foo; include Innate::Node; map '/'; end
700
+ #
701
+ # Foo.patterns_for('/'){|action, params| p action => params }
702
+ # # => {"index"=>[]}
703
+ #
704
+ # Foo.patterns_for('/foo/bar'){|action, params| p action => params }
705
+ # # => {"foo__bar"=>[]}
706
+ # # => {"foo"=>["bar"]}
707
+ # # => {"index"=>["foo", "bar"]}
708
+ #
709
+ # Foo.patterns_for('/foo/bar/baz'){|action, params| p action => params }
710
+ # # => {"foo__bar__baz"=>[]}
711
+ # # => {"foo__bar"=>["baz"]}
712
+ # # => {"foo"=>["bar", "baz"]}
713
+ # # => {"index"=>["foo", "bar", "baz"]}
714
+ #
715
+ # @param [String, #split] path usually the PATH_INFO
716
+ #
717
+ # @return [Action] it actually returns the first non-nil/false result of yield
718
+ #
719
+ # @api internal
720
+ # @see Node#fill_action
721
+ # @author manveru
722
+ def patterns_for(path)
723
+ atoms = path.split('/')
724
+ atoms.delete('')
725
+ result = nil
726
+
727
+ atoms.size.downto(0) do |len|
728
+ action_name = atoms[0...len].join('__')
729
+ params = atoms[len..-1]
730
+ action_name = 'index' if action_name.empty? and params != ['index']
731
+
732
+ return result if result = yield(action_name, params)
733
+ end
734
+
735
+ return nil
736
+ end
737
+
738
+ # Try to find a template at the given +path+ for +wish+.
739
+ #
740
+ # Since Innate supports multiple paths to templates the +path+ has to be an
741
+ # Array that may be nested one level.
742
+ #
743
+ # @example Usage to find available templates
744
+ #
745
+ # # This assumes following files:
746
+ # # view/foo.erb
747
+ # # view/bar.erb
748
+ # # view/bar.rss.erb
749
+ # # view/bar.yaml.erb
750
+ #
751
+ # class FooBar
752
+ # Innate.node('/')
753
+ # end
754
+ #
755
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'html')
756
+ # # => "./view/foo.erb"
757
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'yaml')
758
+ # # => "./view/foo.erb"
759
+ # FooBar.to_template(['.', 'view', '/', 'foo'], 'rss')
760
+ # # => "./view/foo.erb"
761
+ #
762
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'html')
763
+ # # => "./view/bar.erb"
764
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'yaml')
765
+ # # => "./view/bar.yaml.erb"
766
+ # FooBar.to_template(['.', 'view', '/', 'bar'], 'rss')
767
+ # # => "./view/bar.rss.erb"
768
+ #
769
+ # @param [Array<Array<String>>, Array<String>] path
770
+ # array containing strings and nested (1 level) arrays containing strings
771
+ # @param [String] wish
772
+ #
773
+ # @return [nil, String] relative path to the first template found
774
+ #
775
+ # @api external
776
+ # @see Node#find_view Node#to_layout Node#find_aliased_view
777
+ # @author manveru
778
+ def to_template(path, wish)
779
+ to_view(path, wish) || to_layout(path, wish)
780
+ end
781
+
782
+ def update_template_mappings
783
+ update_view_mappings
784
+ update_layout_mappings
785
+ end
786
+
787
+ def update_view_mappings
788
+ paths = possible_paths_for(view_mappings)
789
+ @view_templates = update_mapping_shared(paths)
790
+ end
791
+
792
+ def update_layout_mappings
793
+ paths = possible_paths_for(layout_mappings)
794
+ @layout_templates = update_mapping_shared(paths)
795
+ end
796
+
797
+ def update_mapping_shared(paths)
798
+ mapping = {}
799
+ paths.reject!{|path| !File.directory?(path) }
800
+
801
+ provides.each do |wish_key, engine|
802
+ wish = wish_key[/(.*)_handler/, 1]
803
+ exts = possible_exts_for(wish)
804
+
805
+ paths.reverse_each do |path|
806
+ Find.find(path) do |file|
807
+ exts.each do |ext|
808
+ next unless file =~ ext
809
+
810
+ case file.sub(path, '').gsub('/', '__')
811
+ when /^(.*)\.(.*)\.(.*)$/
812
+ action_name, wish_ext, engine_ext = $1, $2, $3
813
+ when /^(.*)\.(.*)$/
814
+ action_name, wish_ext, engine_ext = $1, wish, $2
815
+ end
816
+
817
+ mapping[wish_ext] ||= {}
818
+ mapping[wish_ext][action_name] = file
819
+ end
820
+ end
821
+ end
822
+ end
823
+
824
+ return mapping
825
+ end
826
+
827
+ # Answer with an array of possible paths in order of significance for
828
+ # template lookup of the given +mappings+.
829
+ #
830
+ # @param [#map] An array two Arrays of inner and outer directories.
831
+ #
832
+ # @return [Array]
833
+ # @see update_view_mappings update_layout_mappings update_template_mappings
834
+ # @author manveru
835
+ def possible_paths_for(mappings)
836
+ root_mappings.map{|root|
837
+ mappings.first.map{|inner|
838
+ mappings.last.map{|outer|
839
+ ::File.join(root, inner, outer, '/') }}}.flatten
840
+ end
841
+
842
+ # Answer with an array of possible extensions in order of significance for
843
+ # the given +wish+.
844
+ #
845
+ # @param [#to_s] wish the extension (no leading '.')
846
+ #
847
+ # @return [Array] list of exts valid for this +wish+
848
+ #
849
+ # @api internal
850
+ # @see Node#to_template View::exts_of Node#provides
851
+ # @author manveru
852
+ def possible_exts_for(wish)
853
+ pr = provides
854
+ return unless engine = pr["#{wish}_handler"]
855
+ View.exts_of(engine).map{|e_ext|
856
+ [[*wish].map{|w_ext| /#{w_ext}\.#{e_ext}$/ }, /#{e_ext}$/]
857
+ }.flatten
858
+ end
859
+
860
+ # For compatibility with new Kernel#binding behaviour in 1.9
861
+ #
862
+ # @return [Binding] binding of the instance being rendered.
863
+ # @see Action#binding
864
+ # @author manveru
865
+ def binding; super end
866
+
867
+ # make sure this is an Array and a new instance so modification on the
868
+ # wrapping array doesn't affect the original option.
869
+ # [*arr].object_id == arr.object_id if arr is an Array
870
+ #
871
+ # @return [Array] list of root directories
872
+ #
873
+ # @api external
874
+ # @author manveru
875
+ def root_mappings
876
+ [*options.roots].flatten
877
+ end
878
+
879
+ # Set the paths for lookup below the Innate.options.views paths.
880
+ #
881
+ # @param [String, Array<String>] locations
882
+ # Any number of strings indicating the paths where view templates may be
883
+ # located, relative to Innate.options.roots/Innate.options.views
884
+ #
885
+ # @return [Node] self
886
+ #
887
+ # @api external
888
+ # @see {Node#view_mappings}
889
+ # @author manveru
890
+ def map_views(*locations)
891
+ trait :views => locations.flatten.uniq
892
+ self
893
+ end
894
+
895
+ # Combine Innate.options.views with either the `ancestral_trait[:views]`
896
+ # or the {Node#mapping} if the trait yields an empty Array.
897
+ #
898
+ # @return [Array<String>, Array<Array<String>>]
899
+ #
900
+ # @api external
901
+ # @see {Node#map_views}
902
+ # @author manveru
903
+ def view_mappings
904
+ paths = [*ancestral_trait[:views]]
905
+ paths = [mapping] if paths.empty?
906
+
907
+ [[*options.views].flatten, [*paths].flatten]
908
+ end
909
+
910
+ # Set the paths for lookup below the Innate.options.layouts paths.
911
+ #
912
+ # @param [String, Array<String>] locations
913
+ # Any number of strings indicating the paths where layout templates may
914
+ # be located, relative to Innate.options.roots/Innate.options.layouts
915
+ #
916
+ # @return [Node] self
917
+ #
918
+ # @api external
919
+ # @see {Node#layout_mappings}
920
+ # @author manveru
921
+ def map_layouts(*locations)
922
+ trait :layouts => locations.flatten.uniq
923
+ self
924
+ end
925
+
926
+ # Combine Innate.options.layouts with either the `ancestral_trait[:layouts]`
927
+ # or the {Node#mapping} if the trait yields an empty Array.
928
+ #
929
+ # @return [Array<String>, Array<Array<String>>]
930
+ #
931
+ # @api external
932
+ # @see {Node#map_layouts}
933
+ # @author manveru
934
+ def layout_mappings
935
+ paths = [*ancestral_trait[:layouts]]
936
+ paths = ['/'] if paths.empty?
937
+
938
+ [[*options.layouts].flatten, [*paths].flatten]
939
+ end
940
+
941
+ def options
942
+ Innate.options
943
+ end
944
+
945
+ # Whether an {Action} can be built without a method.
946
+ #
947
+ # The default is to allow actions that use only a view template, but you
948
+ # might want to turn this on, for example if you have partials in your view
949
+ # directories.
950
+ #
951
+ # @example turning needs_method? on
952
+ #
953
+ # class Foo
954
+ # Innate.node('/')
955
+ # end
956
+ #
957
+ # Foo.needs_method? # => true
958
+ # Foo.trait :needs_method => false
959
+ # Foo.needs_method? # => false
960
+ #
961
+ # @return [true, false] (false)
962
+ #
963
+ # @api external
964
+ # @see {Node#fill_action}
965
+ # @author manveru
966
+ def needs_method?
967
+ ancestral_trait[:needs_method]
968
+ end
969
+
970
+ # This will return true if the only provides set are by {Node::included}.
971
+ #
972
+ # The reasoning behind this is to determine whether the user has touched
973
+ # the provides at all, in which case we will not override the provides in
974
+ # subclasses.
975
+ #
976
+ # @return [true, false] (false)
977
+ #
978
+ # @api internal
979
+ # @see {Node::included}
980
+ # @author manveru
981
+ def provide_set?
982
+ ancestral_trait[:provide_set]
983
+ end
984
+ end
985
+
986
+ module SingletonMethods
987
+ # Convenience method to include the Node module into +node+ and map to a
988
+ # +location+.
989
+ #
990
+ # @param [#to_s] location where the node is mapped to
991
+ # @param [Node, nil] node the class that will be a node, will try to
992
+ # look it up if not given
993
+ #
994
+ # @return [Class, Module] the node argument or detected class will be
995
+ # returned
996
+ #
997
+ # @api external
998
+ # @see SingletonMethods::node_from_backtrace
999
+ # @author manveru
1000
+ def node(location, node = nil)
1001
+ node ||= node_from_backtrace(caller)
1002
+ node.__send__(:include, Node)
1003
+ node.map(location)
1004
+ node
1005
+ end
1006
+
1007
+ # Cheap hack that works reasonably well to avoid passing self all the time
1008
+ # to Innate::node
1009
+ # We simply search the file that Innate::node was called in for the first
1010
+ # class definition above the line that Innate::node was called and look up
1011
+ # the constant.
1012
+ # If there are any problems with this (filenames containing ':' or
1013
+ # metaprogramming) just pass the node parameter explicitly to Innate::node
1014
+ #
1015
+ # @param [Array<String>, #[]] backtrace
1016
+ #
1017
+ # @return [Class, Module]
1018
+ #
1019
+ # @api internal
1020
+ # @see SingletonMethods::node
1021
+ # @author manveru
1022
+ def node_from_backtrace(backtrace)
1023
+ filename, lineno = backtrace[0].split(':', 2)
1024
+ regexp = /^\s*class\s+(\S+)/
1025
+ File.readlines(filename)[0..lineno.to_i].reverse.find{|l| l =~ regexp }
1026
+ const_get($1)
1027
+ end
1028
+ end
1029
+ end