utopia 0.12.6 → 1.0.0

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -2
  3. data/Gemfile +6 -0
  4. data/README.md +48 -14
  5. data/Rakefile +5 -0
  6. data/bin/utopia +132 -15
  7. data/lib/utopia.rb +13 -10
  8. data/lib/utopia/content.rb +140 -0
  9. data/lib/utopia/content/link.rb +124 -0
  10. data/lib/utopia/content/links.rb +228 -0
  11. data/lib/utopia/content/node.rb +387 -0
  12. data/lib/utopia/content/processor.rb +128 -0
  13. data/lib/utopia/content/tag.rb +102 -0
  14. data/lib/utopia/controller.rb +137 -0
  15. data/lib/utopia/controller/action.rb +112 -0
  16. data/lib/utopia/controller/base.rb +174 -0
  17. data/lib/utopia/{middleware/controller → controller}/variables.rb +36 -38
  18. data/lib/utopia/exception_handler.rb +79 -0
  19. data/lib/utopia/extensions/array.rb +2 -2
  20. data/lib/utopia/localization.rb +143 -0
  21. data/lib/utopia/mail_exceptions.rb +136 -0
  22. data/lib/utopia/middleware.rb +7 -22
  23. data/lib/utopia/path.rb +150 -60
  24. data/lib/utopia/redirector.rb +152 -0
  25. data/lib/utopia/{extensions/hash.rb → session.rb} +4 -6
  26. data/lib/utopia/session/encrypted_cookie.rb +46 -48
  27. data/lib/utopia/{middleware/directory_index.rb → session/lazy_hash.rb} +44 -27
  28. data/lib/utopia/static.rb +255 -0
  29. data/lib/utopia/tags/deferred.rb +12 -8
  30. data/lib/utopia/tags/environment.rb +18 -6
  31. data/lib/utopia/tags/node.rb +12 -8
  32. data/lib/utopia/tags/override.rb +12 -12
  33. data/lib/utopia/version.rb +1 -1
  34. data/setup/.bowerrc +3 -0
  35. data/{lib/utopia/setup → setup}/Gemfile +1 -1
  36. data/setup/Rakefile +4 -0
  37. data/{lib/utopia/setup → setup}/cache/head/readme.txt +0 -0
  38. data/{lib/utopia/setup → setup}/cache/meta/readme.txt +0 -0
  39. data/setup/config.ru +64 -0
  40. data/{lib/utopia/setup → setup}/lib/readme.txt +0 -0
  41. data/{lib/utopia/setup → setup}/pages/_heading.xnode +0 -0
  42. data/{lib/utopia/setup → setup}/pages/_page.xnode +1 -1
  43. data/{lib/utopia/setup → setup}/pages/_static/icon.png +0 -0
  44. data/setup/pages/_static/site.css +70 -0
  45. data/{lib/utopia/setup → setup}/pages/errors/exception.xnode +0 -0
  46. data/{lib/utopia/setup → setup}/pages/errors/file-not-found.xnode +0 -0
  47. data/{lib/utopia/setup → setup}/pages/links.yaml +0 -0
  48. data/setup/pages/welcome/index.xnode +17 -0
  49. data/{lib/utopia/setup → setup}/public/readme.txt +0 -0
  50. data/spec/utopia/content/link_spec.rb +108 -0
  51. data/spec/utopia/content/links/foo/index.xnode +0 -0
  52. data/spec/utopia/content/links/foo/links.yaml +2 -0
  53. data/spec/utopia/content/links/foo/test.de.xnode +0 -0
  54. data/spec/utopia/content/links/foo/test.en.xnode +0 -0
  55. data/spec/utopia/content/links/links.yaml +9 -0
  56. data/spec/utopia/content/links/welcome.xnode +0 -0
  57. data/spec/utopia/content/localized/five/index.en.xnode +0 -0
  58. data/spec/utopia/content/localized/four/index.en.xnode +0 -0
  59. data/spec/utopia/content/localized/four/index.zh.xnode +0 -0
  60. data/spec/utopia/content/localized/four/links.yaml +4 -0
  61. data/spec/utopia/content/localized/links.yaml +16 -0
  62. data/spec/utopia/content/localized/one.xnode +0 -0
  63. data/spec/utopia/content/localized/three/index.xnode +0 -0
  64. data/spec/utopia/content/localized/two.en.xnode +0 -0
  65. data/spec/utopia/content/localized/two.zh.xnode +0 -0
  66. data/spec/utopia/content/node/ordered/first.xnode +0 -0
  67. data/spec/utopia/content/node/ordered/index.xnode +0 -0
  68. data/spec/utopia/content/node/ordered/links.yaml +4 -0
  69. data/spec/utopia/content/node/ordered/second.xnode +0 -0
  70. data/spec/utopia/content/node/related/foo.en.xnode +0 -0
  71. data/spec/utopia/content/node/related/foo.ja.xnode +0 -0
  72. data/spec/utopia/content/node/related/links.yaml +4 -0
  73. data/spec/utopia/content/node_spec.rb +63 -0
  74. data/spec/utopia/{middleware/content_spec.rb → content/processor_spec.rb} +34 -23
  75. data/spec/utopia/content_spec.rb +87 -0
  76. data/spec/utopia/content_spec.ru +10 -0
  77. data/spec/utopia/{middleware/controller_spec.rb → controller_spec.rb} +61 -16
  78. data/spec/utopia/controller_spec.ru +4 -0
  79. data/spec/utopia/extensions_spec.rb +6 -17
  80. data/spec/utopia/localization_spec.rb +60 -0
  81. data/spec/utopia/localization_spec.ru +11 -0
  82. data/{lib/utopia/tags.rb → spec/utopia/middleware_spec.rb} +8 -14
  83. data/spec/utopia/{middleware/content_root → pages}/_heading.xnode +0 -0
  84. data/spec/utopia/pages/content/_show-value.xnode +1 -0
  85. data/spec/utopia/pages/content/test-partial.xnode +1 -0
  86. data/spec/utopia/pages/controller/controller.rb +28 -0
  87. data/spec/utopia/pages/controller/index.xnode +1 -0
  88. data/spec/utopia/pages/controller/nested/controller.rb +4 -0
  89. data/spec/utopia/{middleware/content_root → pages}/index.xnode +0 -0
  90. data/spec/utopia/pages/localized.de.txt +1 -0
  91. data/spec/utopia/pages/localized.en.txt +1 -0
  92. data/spec/utopia/pages/localized.jp.txt +1 -0
  93. data/spec/utopia/pages/node/index.xnode +1 -0
  94. data/spec/utopia/pages/test.txt +1 -0
  95. data/spec/utopia/path_spec.rb +109 -0
  96. data/spec/utopia/rack_spec.rb +2 -0
  97. data/spec/utopia/session_spec.rb +82 -0
  98. data/spec/utopia/session_spec.ru +20 -0
  99. data/spec/utopia/spec_helper.rb +16 -0
  100. data/{lib/utopia/extensions/string.rb → spec/utopia/static_spec.rb} +24 -15
  101. data/spec/utopia/static_spec.ru +4 -0
  102. data/utopia.gemspec +3 -3
  103. metadata +138 -54
  104. data/lib/utopia/extensions/regexp.rb +0 -33
  105. data/lib/utopia/link.rb +0 -288
  106. data/lib/utopia/middleware/all.rb +0 -33
  107. data/lib/utopia/middleware/content.rb +0 -157
  108. data/lib/utopia/middleware/content/node.rb +0 -386
  109. data/lib/utopia/middleware/content/processor.rb +0 -123
  110. data/lib/utopia/middleware/controller.rb +0 -130
  111. data/lib/utopia/middleware/controller/action.rb +0 -121
  112. data/lib/utopia/middleware/controller/base.rb +0 -184
  113. data/lib/utopia/middleware/exception_handler.rb +0 -80
  114. data/lib/utopia/middleware/localization.rb +0 -147
  115. data/lib/utopia/middleware/localization/name.rb +0 -69
  116. data/lib/utopia/middleware/mail_exceptions.rb +0 -138
  117. data/lib/utopia/middleware/redirector.rb +0 -146
  118. data/lib/utopia/middleware/requester.rb +0 -126
  119. data/lib/utopia/middleware/static.rb +0 -295
  120. data/lib/utopia/setup.rb +0 -60
  121. data/lib/utopia/setup/config.ru +0 -47
  122. data/lib/utopia/setup/pages/_static/background.png +0 -0
  123. data/lib/utopia/setup/pages/_static/site.css +0 -48
  124. data/lib/utopia/setup/pages/welcome/index.xnode +0 -7
  125. data/lib/utopia/tag.rb +0 -105
  126. data/lib/utopia/tags/all.rb +0 -34
@@ -0,0 +1,128 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'trenni/parser'
22
+ require 'trenni/strings'
23
+
24
+ require_relative 'tag'
25
+
26
+ module Utopia
27
+ class Content
28
+ class Processor
29
+ def self.parse_xml(xml_data, delegate)
30
+ processor = self.new(delegate)
31
+
32
+ processor.parse(xml_data)
33
+ end
34
+
35
+ class UnbalancedTagError < StandardError
36
+ def initialize(scanner, start_position, current_tag, closing_tag)
37
+ @scanner = scanner
38
+ @start_position = start_position
39
+ @current_tag = current_tag
40
+ @closing_tag = closing_tag
41
+
42
+ @starting_line = Trenni::Parser.line_at_offset(@scanner.string, @start_position)
43
+ @ending_line = Trenni::Parser.line_at_offset(@scanner.string, @scanner.pos)
44
+ end
45
+
46
+ attr :scanner
47
+ attr :start_position
48
+ attr :current_tag
49
+ attr :closing_tag
50
+
51
+ def to_s
52
+ "Unbalanced Tag Error. " \
53
+ "Line #{@starting_line[:line_number]}: #{@current_tag} has been closed by #{@closing_tag} on line #{@ending_line[:line_number]}!"
54
+ end
55
+ end
56
+
57
+ def initialize(delegate)
58
+ @delegate = delegate
59
+ @stack = []
60
+
61
+ @parser = Trenni::Parser.new(self)
62
+ end
63
+
64
+ def parse(input)
65
+ @parser.parse(input)
66
+
67
+ unless @stack.empty?
68
+ current_tag, current_position = @stack.pop
69
+
70
+ raise UnbalancedTagError.new(@scanner, current_position, current_tag.name, 'EOF')
71
+ end
72
+ end
73
+
74
+ def begin_parse(scanner)
75
+ @scanner = scanner
76
+ end
77
+
78
+ def text(text)
79
+ @delegate.cdata(text)
80
+ end
81
+
82
+ def cdata(text)
83
+ @delegate.cdata(Trenni::Strings::to_html(text))
84
+ end
85
+
86
+ def comment(text)
87
+ @delegate.cdata("<!#{text}>")
88
+ end
89
+
90
+ def begin_tag(tag_name, begin_tag_type)
91
+ if begin_tag_type == :opened
92
+ @stack << [Tag.new(tag_name, {}), @scanner.pos]
93
+ else
94
+ current_tag, current_position = @stack.pop
95
+
96
+ if tag_name != current_tag.name
97
+ raise UnbalancedTagError.new(@scanner, current_position, current_tag.name, tag_name)
98
+ end
99
+
100
+ @delegate.tag_end(current_tag)
101
+ end
102
+ end
103
+
104
+ def finish_tag(begin_tag_type, end_tag_type)
105
+ if begin_tag_type == :opened # <...
106
+ if end_tag_type == :closed # <.../>
107
+ cur, pos = @stack.pop
108
+ cur.closed = true
109
+
110
+ @delegate.tag_complete(cur)
111
+ elsif end_tag_type == :opened # <...>
112
+ cur, pos = @stack.last
113
+
114
+ @delegate.tag_begin(cur)
115
+ end
116
+ end
117
+ end
118
+
119
+ def attribute(name, value)
120
+ @stack.last[0].attributes[name] = value
121
+ end
122
+
123
+ def instruction(content)
124
+ cdata("<?#{content}?>")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,102 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Utopia
22
+ class Content
23
+ # This represents an individual SGML tag, e.g. <a>, </a> or <a />, with attributes. Attribute values must be escaped.
24
+ class Tag
25
+ def == other
26
+ if Tag === other
27
+ [@name, @attributes, @closed] == [other.name, other.attributes, other.closed]
28
+ end
29
+ end
30
+
31
+ def self.closed(name, attributes = {})
32
+ tag = Tag.new(name, attributes)
33
+ tag.closed = true
34
+
35
+ return tag
36
+ end
37
+
38
+ def initialize(name, attributes = {})
39
+ @name = name
40
+ @attributes = attributes
41
+ @closed = false
42
+ end
43
+
44
+ attr :name
45
+ attr :attributes
46
+ attr :closed, true
47
+
48
+ def [](key)
49
+ @attributes[key]
50
+ end
51
+
52
+ def to_html(content = nil, buffer = StringIO.new)
53
+ write_full_html(buffer, content)
54
+
55
+ return buffer.string
56
+ end
57
+
58
+ def to_hash
59
+ @attributes
60
+ end
61
+
62
+ def to_s(content = nil)
63
+ buffer = StringIO.new
64
+ write_full_html(buffer, content)
65
+ return buffer.string
66
+ end
67
+
68
+ def write_open_html(buffer, terminate = false)
69
+ buffer ||= StringIO.new
70
+ buffer.write "<#{name}"
71
+
72
+ @attributes.each do |key, value|
73
+ if value
74
+ buffer.write " #{key}=\"#{value}\""
75
+ else
76
+ buffer.write " #{key}"
77
+ end
78
+ end
79
+
80
+ if terminate
81
+ buffer.write "/>"
82
+ else
83
+ buffer.write ">"
84
+ end
85
+ end
86
+
87
+ def write_close_html(buffer)
88
+ buffer.write "</#{name}>"
89
+ end
90
+
91
+ def write_full_html(buffer, content = nil)
92
+ if @closed && content == nil
93
+ write_open_html(buffer, true)
94
+ else
95
+ write_open_html(buffer)
96
+ buffer.write(content)
97
+ write_close_html(buffer)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,137 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'path'
22
+
23
+ require_relative 'controller/variables'
24
+ require_relative 'controller/action'
25
+ require_relative 'controller/base'
26
+
27
+ class Rack::Request
28
+ def controller(&block)
29
+ if block_given?
30
+ env["utopia.controller"].instance_eval(&block)
31
+ else
32
+ env["utopia.controller"]
33
+ end
34
+ end
35
+ end
36
+
37
+ module Utopia
38
+ class Controller
39
+ CONTROLLER_RB = "controller.rb".freeze
40
+
41
+ def initialize(app, options = {})
42
+ @app = app
43
+ @root = options[:root] || Utopia::default_root
44
+
45
+ @controllers = {}
46
+
47
+ @cache_controllers = options[:cache_controllers] || false
48
+ end
49
+
50
+ attr :app
51
+
52
+ def lookup_controller(path)
53
+ if @cache_controllers
54
+ return @controllers.fetch(path.to_s) do |key|
55
+ @controllers[key] = load_controller_file(path)
56
+ end
57
+ else
58
+ return load_controller_file(path)
59
+ end
60
+ end
61
+
62
+ def load_controller_file(path)
63
+ uri_path = path
64
+ base_path = File.join(@root, uri_path.components)
65
+
66
+ controller_path = File.join(base_path, CONTROLLER_RB)
67
+ # puts "load_controller_file(#{path.inspect}) => #{controller_path}"
68
+
69
+ if File.exist?(controller_path)
70
+ klass = Class.new(Base)
71
+
72
+ # base_path is expected to be a string representing a filesystem path:
73
+ klass.const_set(:BASE_PATH, base_path)
74
+
75
+ # uri_path is expected to be an instance of Path:
76
+ klass.const_set(:URI_PATH, uri_path)
77
+
78
+ klass.const_set(:CONTROLLER, self)
79
+
80
+ $LOAD_PATH.unshift(base_path)
81
+
82
+ klass.class_eval(File.read(controller_path), controller_path)
83
+
84
+ $LOAD_PATH.delete(base_path)
85
+
86
+ return klass.new
87
+ else
88
+ return nil
89
+ end
90
+ end
91
+
92
+ def invoke_controllers(variables, request, done = Set.new)
93
+ path = Path.create(request.path_info)
94
+ controller_path = Path.new
95
+
96
+ path.descend do |controller_path|
97
+ # puts "Invoke controller: #{controller_path}"
98
+ if controller = lookup_controller(controller_path)
99
+ # We only want to invoke controllers which have not already been invoked:
100
+ unless done.include? controller
101
+ # If we get throw :rewrite, location, the URL has been rewritten and we need to request again:
102
+ location = catch(:rewrite) do
103
+ # Invoke the controller and if it returns a result, send it back out:
104
+ if result = controller.process!(request, path)
105
+ return result
106
+ end
107
+ end
108
+
109
+ if location
110
+ # Rewrite relative paths based on the controller's URI:
111
+ request.env['PATH_INFO'] = Path[location].expand(controller.class.uri_path).to_s
112
+
113
+ return invoke_controllers(variables, request, done)
114
+ end
115
+
116
+ done << controller
117
+ end
118
+ end
119
+ end
120
+
121
+ # No controller gave a useful result:
122
+ return nil
123
+ end
124
+
125
+ def call(env)
126
+ variables = (env["utopia.controller"] ||= Variables.new)
127
+
128
+ request = Rack::Request.new(env)
129
+
130
+ if result = invoke_controllers(variables, request)
131
+ return result
132
+ end
133
+
134
+ return @app.call(env)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,112 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Utopia
22
+ class Controller
23
+ class Action < Hash
24
+ attr_accessor :callback
25
+ attr_accessor :options
26
+
27
+ def callback?
28
+ @callback != nil
29
+ end
30
+
31
+ def indirect?
32
+ @options[:indirect]
33
+ end
34
+
35
+ def eql? other
36
+ super and self.callback.eql? other.callback and self.options.eql? other.options
37
+ end
38
+
39
+ def hash
40
+ [super, callback, options].hash
41
+ end
42
+
43
+ def == other
44
+ super and (self.callback == other.callback) and (self.options == other.options)
45
+ end
46
+
47
+ protected
48
+
49
+ def append(path, index, actions = [])
50
+ # ** is greedy, it always matches if possible:
51
+ if match_all = self[:**]
52
+ # Match all remaining input:
53
+ actions << match_all if match_all.callback?
54
+ end
55
+
56
+ if index < path.size
57
+ name = path[index].to_sym
58
+
59
+ if match_name = self[name]
60
+ # Match the exact name:
61
+ match_name.append(path, index+1, actions)
62
+ end
63
+
64
+ if match_one = self[:*]
65
+ # Match one input:
66
+ match_one.append(path, index+1, actions)
67
+ end
68
+ else
69
+ # Got to end, matched completely:
70
+ actions << self if self.callback?
71
+ end
72
+ end
73
+
74
+ public
75
+
76
+ # relative_path = 2014/mr-potato
77
+ # actions => {:** => A}
78
+ def select(relative_path)
79
+ actions = []
80
+
81
+ append(relative_path.reverse, 0, actions)
82
+
83
+ return actions
84
+ end
85
+
86
+ def define(path, options = {}, &callback)
87
+ current = self
88
+
89
+ path.reverse.each do |name|
90
+ current = (current[name.to_sym] ||= Action.new)
91
+ end
92
+
93
+ current.options = options
94
+ current.callback = callback
95
+
96
+ return current
97
+ end
98
+
99
+ def invoke!(controller, *arguments)
100
+ controller.instance_exec(*arguments, &@callback)
101
+ end
102
+
103
+ def inspect
104
+ if callback?
105
+ "<action " + super + ":#{callback.source_location}(#{options})>"
106
+ else
107
+ "<action " + super + ">"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end