action_tree 0.1.1 → 0.2.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.
data/.yardopts CHANGED
@@ -1,6 +1,3 @@
1
-
2
-
3
-
4
1
  --title "ActionTree"
5
2
 
6
3
  --protected
@@ -8,7 +5,8 @@
8
5
  --markup markdown
9
6
  --markup-provider maruku
10
7
 
11
- lib/**/*.rb -
8
+ --plugin rspec
9
+
10
+ lib/**/*.rb spec/**/*.rb -
12
11
  README.md
13
- MANUAL.md
14
12
  LICENSE
data/README.md CHANGED
@@ -1,88 +1,17 @@
1
- [**CLICK HERE TO READ THE MANUAL**](http://rdoc.info/github/jbe/action_tree/master/file/MANUAL.md)
2
- *ALPHA VERSION. Documentation may be out of date.*
3
- <pre>
4
- \/ | |/
5
- \/ / \||/ /_/___/_
6
- \/ |/ \/
7
- _\__\_\ | /_____/_
8
- \ | / /
9
- __ _-----` |{,-----------~
10
- \ }{
11
- }{{ ACTION TREE:
12
- }}{
13
- {{} a dry request router/controller
14
- , -=-~{ .-^- _
15
- ejm `}
16
- {
17
- </pre>
18
1
 
19
- $ gem install action_tree
2
+ ActionTree
3
+ ==========
20
4
 
21
- Quickstart
22
- ----------
5
+ ActionTree helps you write DRY and concise controller logic. It provides a compact DSL for defining actions and mapping them against the requests that invoke them.
23
6
 
7
+ [**Manual**](https://github.com/jbe/action_tree/wiki)
24
8
 
25
- The Local Office of Alligators, division of information, subdivision of web2.0 and social media, have drawn a (naive) map for their new alligator web API:
9
+ [**API docs**](http://rubydoc.info/github/jbe/action_tree/master/frames)
26
10
 
27
-
28
- welcome
29
- |-- alligators
30
- | |-- list
31
- | |-- create
32
- | `-- (with an id)
33
- | |-- edit
34
- | `-- hug
35
-
36
- A map like this not only shows paths, but also relationships between actions. For instance, `edit` and `hug` retrieve a specific alligator (with an id) before doing anything else. Similarly, every request beneath `/alligators/` should be counted, to see if we're on Digg yet.
37
-
38
- So let's put those two in `before` hooks in an ActionTree:
39
-
40
- routes = ActionTree.new do
41
- action { "Welcome to the alligator manager #{@name}!" }
42
-
43
- with 'alligators' do
44
- before { @count = (@count || 0) + 1 }
45
-
46
- action { Alligator.all }
47
- action('create') { Alligator.create }
48
-
49
- with :id do
50
- before { @alligator = Alligator.get(@id) }
51
- action { 'this is ' + @alligator }
52
- action('edit') { "editing #{@alligator}" }
53
- action('hug') { "#{@alligator} is pleased." }
54
- end
55
- end
56
- end
57
-
58
- We can then match and run actions:
59
-
60
- routes.match('/') #=> #<ActionTree::Match >
61
- routes.match('/').found? #=> true
62
-
63
- @name = 'Zap'
64
- routes.match('/').run(self) # => "Welcome to the alligator manager, Zap."
65
-
66
- routes.match('/alligators/4').run # => "this is #<Alligator 4>"
67
- router.match('/alligators/8/hug').run # => "<#Alligator 8> is pleased."
68
-
69
- routes.match('nonexistant/route') # => #<ActionTree::NotFound>
70
- routes.match('nonexistant/route').found? #=> false
71
-
72
-
73
- &nbsp;
74
-
75
-
76
- Available dialects
77
- ------------------
78
-
79
- [**at-rack**](https://github.com/jbe/at-rack) turns ActionTree into a Sinatra-like framework, and allows you to mount trees as rack apps.
80
-
81
- **at-websocket** is planned to make writing websocket servers easier.
11
+ *NB: Alpha stage. Unstable API.*
82
12
 
83
13
  &nbsp;
84
14
 
85
15
  ---
86
16
 
87
17
  Copyright (c) 2010-2011 Jostein Berre Eliassen. See [LICENSE](http://rdoc.info/github/jbe/action_tree/master/file/LICENSE) for details.
88
-
data/Rakefile CHANGED
@@ -5,8 +5,8 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "action_tree"
8
- gem.summary = %Q{Versatile routing dry enough to kill cacti.}
9
- gem.description = %Q{ActionTree is a router. It provides a compact DSL for defining routes that map paths to actions. It was designed as a router for modern ruby mini-frameworks.}
8
+ gem.summary = %Q{Versatile and DRY controllers and routing.}
9
+ gem.description = %Q{ActionTree is a DRY request router. It provides a compact DSL for defining actions and mapping requests to invoke them.}
10
10
  gem.email = "post@jostein.be"
11
11
  gem.homepage = "http://github.com/jbe/action_tree"
12
12
  gem.authors = ["jbe"]
@@ -14,6 +14,10 @@ begin
14
14
  #gem.required_ruby_version = '>= 1.8.7'
15
15
  gem.add_dependency 'backports'
16
16
 
17
+ gem.add_development_dependency 'rspec'
18
+ gem.add_development_dependency 'yard'
19
+ gem.add_development_dependency 'yard-rspec'
20
+
17
21
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
22
  end
19
23
  Jeweler::GemcutterTasks.new
@@ -31,10 +35,19 @@ end
31
35
  require 'rspec/core/rake_task'
32
36
  RSpec::Core::RakeTask.new(:spec)
33
37
 
38
+ require 'yard'
39
+ YARD::Rake::YardocTask.new do |t|
40
+ #t.files = ['lib/**/*.rb', OTHER_PATHS] # optional
41
+ #t.options = ['--any', '--extra', '--opts'] # optional
42
+ end
34
43
 
35
44
  task :spec => :check_dependencies
36
- task :default => :spec
37
45
 
46
+ task :docs => :yard
47
+
48
+ task :build => :docs
49
+
50
+ task :default => :spec
38
51
 
39
52
 
40
53
  #namespace :test do
data/TODO CHANGED
@@ -1,32 +1,90 @@
1
1
 
2
2
 
3
3
 
4
+ porting sinatra
5
+ code: l 59
6
+ HTTP Verb helpers
7
+ halt(code)
8
+ specs: l 0
4
9
 
5
- dialect mixins
6
- templating
7
-
8
- rewrite tests
9
- tests for:
10
- variable scope copy
11
- after hooks
12
- action namespaces
13
- running different action layers
14
- get, put, post, delete
15
- postprocessors
16
- mounting
17
- layers & application
18
-
19
-
20
- documentation for:
21
- postprocessors
22
- action namespaces
23
- new mounting
24
- layer application
25
- ActionTree.layer
26
- Node.apply(layer)
27
- mounting versus layer application
28
- get, put, post, and update helpers
29
- DSL shorthands/aliases
30
- applying plugins
31
10
 
11
+ code:
12
+ Basic
13
+ finish documenting Node
14
+ Give Match a wash
15
+ document Match
16
+ Templates
17
+ raise errors:
18
+ template not found
19
+
20
+ display errors?
32
21
 
22
+
23
+ documentation
24
+ Rack
25
+ use Rack::ShowExceptions for html error pages.
26
+ `use` belongs in config.ru :)
27
+
28
+ ideas
29
+ `at-rack-serve` command-line tool
30
+
31
+
32
+ ActionTree spec outline
33
+ =======================
34
+
35
+ 00 Foundations [DONE]
36
+
37
+ 01 Node
38
+
39
+ #inspect
40
+ #printout
41
+
42
+ #locate
43
+
44
+ - for string, symbol, sinatra-style, nasty
45
+ sinatra-style regexp, and nasty regexp
46
+ #match?
47
+ #capture_names
48
+ #read_captures
49
+
50
+ dsl
51
+ #route
52
+ #mount
53
+ when children have the same name
54
+ #add_child
55
+
56
+ 02 Match
57
+
58
+ #run
59
+ not_found
60
+ actions
61
+ namespaces
62
+ exception handling
63
+ scope
64
+ user supplied
65
+ hash sets instance variables
66
+ module is extended
67
+ array is both
68
+ captures
69
+ sinatra style
70
+ regex style
71
+ accumulate
72
+
73
+
74
+ 03 ActionTree::Basic
75
+ scenario with:
76
+ captures: string, symbol, nasty sinatra-style
77
+ building
78
+ using: array paths, regex paths, string paths, symbol paths
79
+
80
+ config: inheritance, overwrite
81
+
82
+ actions: namespaces, after, before, postprocessors, deep
83
+
84
+ fancy: mounting
85
+
86
+ run-time: user vars, priority, @config
87
+
88
+ 04 Components
89
+ tilt
90
+ http verb
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -1,45 +1,64 @@
1
1
 
2
+ #
3
+ # \/ | |/
4
+ # \/ / \||/ /_/___/_
5
+ # \/ |/ \/
6
+ # _\__\_\ | /_____/_
7
+ # \ | / /
8
+ # __ _-----` |{,-----------~
9
+ # \ }{
10
+ # }{{
11
+ # }}{ ActionTree (c) 2011
12
+ # {{} Jostein Berre Eliassen
13
+ # , -=-~{ .-^- _
14
+ # ejm `}
15
+ # {
16
+ #
17
+
2
18
  require 'set'
3
19
  require 'backports'
4
20
 
21
+ # The main ActionTree namespace. Provides convenient
22
+ # shortcuts to common operations. Contains utility classes
23
+ # and dialect modules. For more on dialects, see {Basic}.
24
+ #
25
+ # @see https://github.com/jbe/action_tree/wiki The ActionTree Wiki
26
+
5
27
  module ActionTree
6
28
 
7
29
  require 'action_tree/eval_scope'
8
30
  require 'action_tree/capture_hash'
9
- require 'action_tree/dialect_helper'
10
-
11
- module Plugins
12
- require 'action_tree/plugins/tilt'
13
- end
31
+ require 'action_tree/errors'
32
+ require 'action_tree/basic'
14
33
 
34
+ include Errors
15
35
 
16
- module Dialect
17
- def new(*prms, &blk)
18
- self::Node.new(*prms, &blk)
19
- end
20
- def apply(mod)
21
- self::Node.send(:include, mod::NodeMixin)
22
- self::Match.send(:include, mod::MatchMixin)
23
- self::Match::DEFAULT_HELPERS << mod::Helpers
24
- end
25
- end
26
-
27
- # load basic dialect
28
- module Basic
29
- extend Dialect
30
- require "action_tree/basic/node"
31
- require "action_tree/basic/match"
32
- end
36
+ # @private
37
+ # Named procs that can be applied to trees.
38
+ MACROS = {}
33
39
 
34
- # shorthand
40
+ # Create a new {Basic} ActionTree.
41
+ #
42
+ # @yield Constructs the tree from the given block of directives
43
+ #
44
+ # @example
45
+ # a = ActionTree.new do
46
+ # action('hi') { "Hello!" }
47
+ # end
48
+ # a.match('hi').run #=> "Hello!"
49
+ #
50
+ # @see Basic::Node
51
+ #
52
+ # @return [Basic::Node] Root node of the created tree
35
53
  def self.new(&blk)
36
54
  Basic.new(&blk)
37
55
  end
38
56
 
39
- # layer proc wrapper
40
- class Layer < Proc; end
41
- def layer(*prms, &blk)
42
- Layer.new(*prms, &blk)
57
+ # Create an optionally named macro. If a name is given, the macro is remembered by that name, and can later be referenced using {Basic::Node#apply}.
58
+ #
59
+ # @return [Proc] The macro
60
+ def self.macro(name=nil, &blk)
61
+ name ? (MACROS[name] = blk) : blk
43
62
  end
44
63
 
45
64
  end
@@ -0,0 +1,36 @@
1
+
2
+ # The basic ActionTree namespace that other dialects build upon. Contains default config values, default helpers, and the {Node} and {Query} classes that make up ActionTree's main logic.
3
+ #
4
+ # @see https://github.com/jbe/action_tree/wiki/Dialects-and-components Wiki page about dialects and components
5
+ #
6
+
7
+ module ActionTree::Basic
8
+
9
+ CONFIG = {}
10
+ require 'action_tree/basic/helpers'
11
+
12
+ module ClassMethods
13
+
14
+ # Create a new ActionTree (or rather, {Node})
15
+ # of this dialect
16
+ def new(*prms, &blk)
17
+ self::Node.new(*prms, &blk)
18
+ end
19
+
20
+ # Use a component. The component will be used by all
21
+ # descending dialects, and must not be used again.
22
+ def use(component)
23
+ self::Node.send :include, component::NodeMethods
24
+ self::Query.send :include, component::QueryMethods
25
+ self::Helpers.send :include, component::Helpers
26
+ self::CONFIG.merge! component::CONFIG
27
+ end
28
+ end
29
+
30
+ extend ClassMethods
31
+
32
+ require "action_tree/basic/shared"
33
+ require "action_tree/basic/node"
34
+ require "action_tree/basic/query"
35
+ end
36
+
@@ -0,0 +1,20 @@
1
+
2
+ # Contains the default helpers available inside an ActionTree
3
+ # execution scope.
4
+ #
5
+ # @see https://github.com/jbe/action_tree/wiki/Basic-usage Wiki on basic usage
6
+
7
+ module ActionTree::Basic::Helpers
8
+
9
+ # Writes a configuration value if it
10
+ # has not been set already
11
+ def default(key, value)
12
+ @_conf[key] ||= value
13
+ end
14
+
15
+ # Writes or overwrites a configuration value
16
+ def set(key, value)
17
+ @_conf[key] = value
18
+ end
19
+
20
+ end
@@ -1,167 +1,358 @@
1
1
 
2
+
3
+ # Every ActionTree fundamentally consists of Nodes. The {Node} class
4
+ # contains the methods used to build trees (the DSL). It is the
5
+ # core of ActionTree.
6
+ #
7
+ # @see https://github.com/jbe/action_tree/wiki The ActionTree Wiki
8
+
2
9
  class ActionTree::Basic::Node
3
10
 
4
- include ::ActionTree::DialectHelper
11
+ include ActionTree::Basic::Shared
12
+ include ActionTree::Errors
13
+
14
+ # The token that request path fragments will be matched against.
15
+ # @return [String, Symbol, Regex]
16
+ attr_reader :token
17
+
18
+ # The nodes children; representing its direct sub-paths.
19
+ # @return [Set]
20
+ attr_reader :children
21
+
22
+ # The module containing any user defined helpers.
23
+ # @return [Module]
24
+ attr_reader :helper_scope
25
+
26
+ # The configuration values set for the node.
27
+ # @return [Hash]
28
+ attr_reader :config
29
+
30
+ # The actions defined for the node. `nil` is used for the default action.
31
+ # @return [Hash]
32
+ attr_reader :actions
33
+
34
+ # The procs to be invoked before the action is run.
35
+ # @return [Array]
36
+ attr_reader :before_hooks
37
+
38
+ # The procs to be invoked after the action is run.
39
+ # @return [Array]
40
+ attr_reader :after_hooks
41
+
42
+ # The procs to chain-process the result of running the action.
43
+ # @return [Array]
44
+ attr_reader :processors
45
+
46
+ # Same as {Node#processors}, but also processes child nodes.
47
+ # @return [Array]
48
+ attr_reader :deep_processors
5
49
 
6
- attr_reader :token, :children, :helper_scope,
7
- :action_namespaces, :before_hooks,
8
- :after_hooks, :postprocessors
9
- attr_accessor :not_found_handler
50
+ # The hash of procs that will handle exceptions in child node actions.
51
+ # @return [Hash] where `ErrorClass => proc {|err| handler }`
52
+ attr_accessor :exception_handlers
10
53
 
11
54
 
12
- def initialize(path=[], &blk)
13
- path = parse_path(path)
14
- @token = path.shift
15
- @children = Set.new
16
- @helper_scope = Module.new
17
- @before_hooks = []
18
- @after_hooks = []
19
- @postprocessors = []
20
- @action_namespaces = {}
21
- @not_found_handler = nil
22
- route(path, &blk) if block_given?
55
+ # Create a new Node or tree of Nodes.
56
+ #
57
+ # @param [nil, String, Symbol, Regexp] token An optional node token. Not needed for root nodes unless the it is later mounted as a child somewhere.
58
+ #
59
+ # @yield Takes an optional block to be evaluated inside the Node. This is used to construct the ActionTree, using the "DSL".
60
+ #
61
+ def initialize(token=nil, &blk)
62
+ @token = token # string, symbol or regex
63
+ @children = Set.new # set of nodes
64
+ @helper_scope = Module.new
65
+ @config = {}
66
+ @actions = {} # {:name => proc {...}}
67
+ @before_hooks = [] # [procs] without params
68
+ @after_hooks = [] # [procs] without params
69
+ @processors = [] # [procs] with one param
70
+ @deep_processors = [] # ditto
71
+ @exception_handlers = {} # {ErrClass => proc {...}}
72
+ route([], &blk) if blk
23
73
  end
24
74
 
25
- # INSPECTION
26
75
 
76
+ ## INSPECTION
77
+
78
+ # String representation for inspecting the node.
79
+ # @return [String]
27
80
  def inspect
28
81
  "#<#{self.class}:#{self.object_id.to_s(16)} #{@token.inspect} >"
29
82
  end
30
83
 
84
+ # A tree-like multiline string of descending nodes.
85
+ # @return [String]
31
86
  def printout(stack='')
32
87
  stack + @token.inspect + "\n" +
33
88
  @children.map {|c| c.printout(stack + ' ') }.join
34
89
  end
35
90
 
36
- # MATCHING
37
-
38
- def regexp
39
- @regexp ||=
40
- Regexp.new('^' + case @token
41
- when Regexp then "(#{@token.source})"
42
- when Symbol then '(.+)'
43
- when String then @token.gsub(/:\w+/, '(.+)')
44
- #when nil then '\/*'
45
- end + '$')
46
- end
47
91
 
48
- def match?(path_fragment)
49
- !!path_fragment.match(regexp)
50
- end
92
+ ## DSL
51
93
 
52
- def capture_names
53
- case @token
54
- when Regexp then ['match']
55
- when Symbol then [@token.to_s]
56
- when String
57
- @token.scan(/:\w+/).map {|m| m[1..-1] }
94
+ # Find or create a descending node.
95
+ #
96
+ # @param [Array, String, Symbol, Regexp, nil] location
97
+ # A Node location relative to this Node
98
+ #
99
+ # @see https://github.com/jbe/action_tree/wiki/Definitions Location format specification in the Wiki
100
+ #
101
+ # @return [Node] at the given relative location.
102
+ # Created automatically if it does not exist.
103
+ def locate(location)
104
+ loc = parse_path(location)
105
+ if loc.empty? then self else
106
+ token = loc.shift
107
+ (get_child(token) || make_child(token)).locate(loc)
58
108
  end
59
109
  end
60
-
61
-
62
- # DSL
63
-
110
+
111
+ # Evaluate the given block in a descending node,
112
+ # at the specified `location`.
113
+ #
114
+ # @param location (see #locate)
115
+ # @yield Takes a block to be evaluated at `location`
116
+ #
117
+ # @return [Node] self
64
118
  def route(location=nil, &blk)
65
- descend(location).instance_eval(&blk) if blk
119
+ if blk.nil? && location.is_a?(Proc)
120
+ blk = location
121
+ location = nil
122
+ end
123
+ locate(location).instance_eval(&blk)
66
124
  self
67
125
  end
68
- alias :with :route
69
- alias :w :route
70
- alias :r :route
71
- alias :_ :route
72
- def apply(layer); route(&layer); end
126
+ alias :with :route
127
+ alias :w :route
128
+ alias :r :route
129
+
130
+ # Apply a (named) `Proc`, referred to as a macro.
131
+ #
132
+ # @param [Proc, Object] macro
133
+ # A Proc or the name of a stored proc (defined using
134
+ # {ActionTree.macro}) to be evaluated in the node.
135
+ #
136
+ def apply(macro)
137
+ case macro
138
+ when Proc then route(&macro)
139
+ else route(&::ActionTree::MACROS[macro])
140
+ end
141
+ end
73
142
 
74
- def action(location=nil, namespace=:default, &blk)
75
- descend(location).action_namespaces[namespace] = blk
143
+ # Define an optionally namespaced action, optionally at
144
+ # the specified `location`.
145
+ #
146
+ # @param location (see #locate)
147
+ # @param namespace The name of the action. Used to
148
+ # place actions within a namespace (e.g. for HTTP verbs).
149
+ #
150
+ # @yield Takes a block containing the action.
151
+ #
152
+ # @return [Node] self
153
+ def action(location=nil, namespace=nil, &blk)
154
+ locate(location).actions[namespace] = blk
76
155
  self
77
156
  end
78
157
  alias :a :action
79
- alias :o :action
80
- def get( loc=nil, &blk); action(loc, :get, &blk); end
81
- def put( loc=nil, &blk); action(loc, :put, &blk); end
82
- def post( loc=nil, &blk); action(loc, :post, &blk); end
83
- def delete(loc=nil, &blk); action(loc, :delete, &blk); end
84
158
 
159
+ # Define helper methods
160
+ # Open a scope for defining helper methods, optionally at the specified `location`. They will be available to this node, as well as all descendants.
161
+ #
162
+ # @param location (see #locate)
163
+ #
164
+ # @yield Takes a block containing helper method definitions.
165
+ #
166
+ # @example
167
+ # a = ActionTree.new do
168
+ # helpers do
169
+ # def sing
170
+ # 'lala'
171
+ # end
172
+ # end
173
+ #
174
+ # action { sing }
175
+ # end
176
+ #
177
+ # a.match('/').run # => "lala"
178
+ #
85
179
  def helpers(location=nil, &blk)
86
- descend(location).helper_scope.module_eval(&blk)
180
+ locate(location).helper_scope.module_eval(&blk)
87
181
  end
88
182
 
183
+ # Add a before hook, optionally at the specified `location`. It will apply to this node, as well as all descendants.
184
+ #
185
+ # @param location (see #locate)
186
+ #
187
+ # @yield Takes a block containing the hook to be run.
89
188
  def before(location=nil, &blk)
90
- descend(location).before_hooks << blk if blk
189
+ locate(location).before_hooks << blk
91
190
  end
92
191
  alias :b :before
93
192
 
193
+ # Add an after hook, optionally at the specified `location`. Like the {Node#before} hook, it will also apply to all descendants.
194
+ #
195
+ # @param location (see #locate)
196
+ #
197
+ # @yield (see #before)
94
198
  def after(location=nil, &blk)
95
- descend(location).after_hooks << blk if blk
199
+ locate(location).after_hooks << blk
200
+ end
201
+ alias :af :after
202
+
203
+ # Add an exception handler
204
+ #
205
+ # @example
206
+ # handle(MyException) do |err|
207
+ # "oops, #{err}"
208
+ # end
209
+ def handle(error_class, &blk)
210
+ unless error_class.ancestors.include?(Exception)
211
+ raise ArgumentError, "#{error_class.inspect} is not an exception."
212
+ end
213
+ @exception_handlers[error_class] = blk
214
+ end
215
+ alias :h :handle
216
+
217
+ # Add a processor
218
+ def processor(location=nil, &blk)
219
+ locate(location).processors << blk
96
220
  end
221
+ alias :p :processor
97
222
 
98
- def not_found(location=nil, &blk)
99
- descend(location).not_found_handler = blk if blk
223
+ # Add a deep processor
224
+ def deep_processor(location=nil, &blk)
225
+ locate(location).deep_processors << blk
100
226
  end
101
- alias :x :not_found
227
+ alias :dp :deep_processor
102
228
 
103
- def postprocessor(location=nil, &blk)
104
- descend(location).postprocessors << blk if blk
229
+ # Set a configuration value
230
+ def set(key, value)
231
+ config[key] = value
105
232
  end
106
- alias :p :postprocessor
107
233
 
234
+ # Add the children of another Node to the children
235
+ # of this Node.
108
236
  def mount(node, location=nil)
109
- if node.token
110
- descend(location).children < node
111
- else
112
- raise 'root nodes can not be mounted. apply a layer instead.'
113
- end
237
+ locate(location).add_child(*node.children)
238
+ end
239
+
240
+ # Add one or several children to the node, provided
241
+ # they are compatible (i.e. of the same dialect).
242
+ def add_child(*nodes)
243
+ nodes.each {|n| validate_child(n) }
244
+ children << nodes
114
245
  end
115
246
 
116
-
117
- # LOOKUPS
118
247
 
119
- def match(path=[])
120
- dialect::Match.new(self, nil, nil).match(path)
248
+ ## MATCHING METHODS
249
+
250
+ # Match a against a request path, building a chain of {Query} objects.
251
+ #
252
+ # @param [String, Array] path The lookup path to query.
253
+ # @param namespace The action namespace.
254
+ #
255
+ # @return [Query] the result (even when not found).
256
+ def match(path=[], namespace=nil)
257
+ dialect::Query.new(self, nil, nil, namespace).match(path)
121
258
  end
259
+ alias :query :match
122
260
 
123
- def descend(path)
124
- loc = parse_path(path)
125
- if loc.empty? then self else
126
- token = loc.shift
127
- (get_child(token) || make_child(token)).descend(loc)
261
+
262
+
263
+ # === End of public API ===
264
+
265
+
266
+ # Check if a path fragment matches the node token.
267
+ # @private
268
+ def match?(path_fragment)
269
+ path_fragment.match(regexp)
270
+ end
271
+
272
+ # The names that this node will capture to.
273
+ # @private
274
+ def capture_names
275
+ @capture_names ||= case @token
276
+ when Regexp then ['match']
277
+ when Symbol then [@token.to_s]
278
+ when String
279
+ @token.scan(/:\w+/).map {|m| m[1..-1] }
128
280
  end
129
281
  end
130
282
 
283
+ # Reads captures from a path fragment, storing them
284
+ # under their names in a {CaptureHash}
285
+ # @private
286
+ def read_captures(fragment, hsh)
287
+ return hsh if capture_names.empty?
288
+ captures = fragment.match(regexp).captures
289
+ capture_names.each_with_index do |name, i|
290
+ hsh.add(name, captures[i])
291
+ end; hsh
292
+ end
131
293
 
132
294
  private
133
295
 
134
- def get_child(token)
135
- @children.each do |child|
136
- return child if child.token == token
137
- end; nil
138
- end
296
+ # regexp to see if a path fragment matches
297
+ # this node's token
298
+ def regexp
299
+ @regexp ||=
300
+ Regexp.new('^' + case @token
301
+ when Regexp then "(#{@token.source})"
302
+ when Symbol then '(.+)'
303
+ when String then @token.gsub(/:\w+/, '(.+)')
304
+ end + '$')
305
+ end
139
306
 
140
- def make_child(token)
141
- child = self.class.new(token)
142
- @children << child
143
- child
307
+ def get_child(token)
308
+ @children.each do |child|
309
+ return child if child.token == token
310
+ end; nil
311
+ end
312
+
313
+ def make_child(token)
314
+ child = self.class.new(token)
315
+ @children << child
316
+ child
317
+ end
318
+
319
+ # convert any valid path syntax to array syntax
320
+ def parse_path(path)
321
+ case path
322
+ when nil then []
323
+ when Array then path
324
+ when Regexp, Symbol then [path]
325
+ when String then parse_string_path(path)
326
+ else
327
+ raise "invalid path #{path}"
144
328
  end
329
+ end
145
330
 
146
- # convert any valid path syntax to array syntax
147
- def parse_path(path)
148
- case path
149
- when nil then []
150
- when Array then path
151
- when Regexp, Symbol then [path]
152
- when String then parse_string_path(path)
153
- else
154
- raise "invalid path #{path}"
331
+ # convert a string to array path syntax
332
+ def parse_string_path(path)
333
+ path.gsub(/(^\/)|(\/$)/, ''). # remove trailing slashes
334
+ split('/').map do |str| # split tokens
335
+ str.match(/^:\w+$/) ? # replace ':c' with :c
336
+ str[1..-1].to_sym : str
155
337
  end
156
- end
338
+ end
157
339
 
158
- # convert a string to array path syntax
159
- def parse_string_path(path)
160
- path.gsub(/(^\/)|(\/$)/, ''). # remove trailing slashes
161
- split('/').map do |str| # split tokens
162
- str.match(/^:\w+$/) ? # replace ':c' with :c
163
- str[1..-1].to_sym : str
164
- end
340
+ # raises errors if the provided node for some
341
+ # reason cannot be added as a child to this node.
342
+ def validate_child(node)
343
+ case
344
+ when !self.compatible_with(node)
345
+ raise "cannot add child with class " +
346
+ "#{node.class} to #{self.inspect}"
347
+ when children.map(&:token).include?(node.token)
348
+ raise 'cannot add child with occupied token: ' +
349
+ node.token.inspect
350
+ else true
165
351
  end
352
+ end
353
+
354
+ def compatible_with?(node)
355
+ node.class == self.class
356
+ end
166
357
  end
167
358