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