utopia 0.12.6 → 1.0.0

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