# Every ActionTree fundamentally consists of Nodes. The {Node} class # contains the methods used to build trees (the DSL). It is the # core of ActionTree. # # @see https://github.com/jbe/action_tree/wiki The ActionTree Wiki class ActionTree::Basic::Node include ActionTree::Basic::Shared include ActionTree::Errors # The token that request path fragments will be matched against. # @return [String, Symbol, Regex] attr_reader :token # The nodes children; representing its direct sub-paths. # @return [Set] attr_reader :children # The module containing any user defined helpers. # @return [Module] attr_reader :helper_scope # The configuration values set for the node. # @return [Hash] attr_reader :config # The actions defined for the node. `nil` is used for the default action. # @return [Hash] attr_reader :actions # The procs to be invoked before the action is run. # @return [Array] attr_reader :before_hooks # The procs to be invoked after the action is run. # @return [Array] attr_reader :after_hooks # The procs to chain-process the result of running the action. # @return [Array] attr_reader :processors # Same as {Node#processors}, but also processes child nodes. # @return [Array] attr_reader :deep_processors # The hash of procs that will handle exceptions in child node actions. # @return [Hash] where `ErrorClass => proc {|err| handler }` attr_accessor :exception_handlers # Create a new Node or tree of Nodes. # # @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. # # @yield Takes an optional block to be evaluated inside the Node. This is used to construct the ActionTree, using the "DSL". # def initialize(token=nil, &blk) @token = token # string, symbol or regex @children = Set.new # set of nodes @helper_scope = Module.new @config = {} @actions = {} # {:name => proc {...}} @before_hooks = [] # [procs] without params @after_hooks = [] # [procs] without params @processors = [] # [procs] with one param @deep_processors = [] # ditto @exception_handlers = {} # {ErrClass => proc {...}} route([], &blk) if blk end ## INSPECTION # String representation for inspecting the node. # @return [String] def inspect "#<#{self.class}:#{self.object_id.to_s(16)} #{@token.inspect} >" end # A tree-like multiline string of descending nodes. # @return [String] def printout(stack='') stack + @token.inspect + "\n" + @children.map {|c| c.printout(stack + ' ') }.join end ## DSL # Find or create a descending node. # # @param [Array, String, Symbol, Regexp, nil] location # A Node location relative to this Node # # @see https://github.com/jbe/action_tree/wiki/Definitions Location format specification in the Wiki # # @return [Node] at the given relative location. # Created automatically if it does not exist. def locate(location) loc = parse_path(location) if loc.empty? then self else token = loc.shift (get_child(token) || make_child(token)).locate(loc) end end # Evaluate the given block in a descending node, # at the specified `location`. # # @param location (see #locate) # @yield Takes a block to be evaluated at `location` # # @return [Node] self def route(location=nil, &blk) if blk.nil? && location.is_a?(Proc) blk = location location = nil end locate(location).instance_eval(&blk) self end alias :with :route alias :w :route alias :r :route # Apply a (named) `Proc`, referred to as a macro. # # @param [Proc, Object] macro # A Proc or the name of a stored proc (defined using # {ActionTree.macro}) to be evaluated in the node. # def apply(macro) case macro when Proc then route(¯o) else route(&::ActionTree::MACROS[macro]) end end # Define an optionally namespaced action, optionally at # the specified `location`. # # @param location (see #locate) # @param namespace The name of the action. Used to # place actions within a namespace (e.g. for HTTP verbs). # # @yield Takes a block containing the action. # # @return [Node] self def action(location=nil, namespace=nil, &blk) locate(location).actions[namespace] = blk self end alias :a :action # Define helper methods # Open a scope for defining helper methods, optionally at the specified `location`. They will be available to this node, as well as all descendants. # # @param location (see #locate) # # @yield Takes a block containing helper method definitions. # # @example # a = ActionTree.new do # helpers do # def sing # 'lala' # end # end # # action { sing } # end # # a.match('/').run # => "lala" # def helpers(location=nil, &blk) locate(location).helper_scope.module_eval(&blk) end # Add a before hook, optionally at the specified `location`. It will apply to this node, as well as all descendants. # # @param location (see #locate) # # @yield Takes a block containing the hook to be run. def before(location=nil, &blk) locate(location).before_hooks << blk end alias :b :before # Add an after hook, optionally at the specified `location`. Like the {Node#before} hook, it will also apply to all descendants. # # @param location (see #locate) # # @yield (see #before) def after(location=nil, &blk) locate(location).after_hooks << blk end alias :af :after # Add an exception handler # # @example # handle(MyException) do |err| # "oops, #{err}" # end def handle(error_class, &blk) unless error_class.ancestors.include?(Exception) raise ArgumentError, "#{error_class.inspect} is not an exception." end @exception_handlers[error_class] = blk end alias :h :handle # Add a processor def processor(location=nil, &blk) locate(location).processors << blk end alias :p :processor # Add a deep processor def deep_processor(location=nil, &blk) locate(location).deep_processors << blk end alias :dp :deep_processor # Set a configuration value def set(key, value) config[key] = value end # Add the children of another Node to the children # of this Node. def mount(node, location=nil) locate(location).add_child(*node.children) end # Add one or several children to the node, provided # they are compatible (i.e. of the same dialect). def add_child(*nodes) nodes.each {|n| validate_child(n) } children << nodes end ## MATCHING METHODS # Match a against a request path, building a chain of {Query} objects. # # @param [String, Array] path The lookup path to query. # @param namespace The action namespace. # # @return [Query] the result (even when not found). def match(path=[], namespace=nil) dialect::Query.new(self, nil, nil, namespace).match(path) end alias :query :match # === End of public API === # Check if a path fragment matches the node token. # @private def match?(path_fragment) path_fragment.match(regexp) end # The names that this node will capture to. # @private def capture_names @capture_names ||= case @token when Regexp then ['match'] when Symbol then [@token.to_s] when String @token.scan(/:\w+/).map {|m| m[1..-1] } end end # Reads captures from a path fragment, storing them # under their names in a {CaptureHash} # @private def read_captures(fragment, hsh) return hsh if capture_names.empty? captures = fragment.match(regexp).captures capture_names.each_with_index do |name, i| hsh.add(name, captures[i]) end; hsh end private # regexp to see if a path fragment matches # this node's token def regexp @regexp ||= Regexp.new('^' + case @token when Regexp then "(#{@token.source})" when Symbol then '(.+)' when String then @token.gsub(/:\w+/, '(.+)') end + '$') end def get_child(token) @children.each do |child| return child if child.token == token end; nil end def make_child(token) child = self.class.new(token) @children << child child end # convert any valid path syntax to array syntax def parse_path(path) case path when nil then [] when Array then path when Regexp, Symbol then [path] when String then parse_string_path(path) else raise "invalid path #{path}" end end # convert a string to array path syntax def parse_string_path(path) path.gsub(/(^\/)|(\/$)/, ''). # remove trailing slashes split('/').map do |str| # split tokens str.match(/^:\w+$/) ? # replace ':c' with :c str[1..-1].to_sym : str end end # raises errors if the provided node for some # reason cannot be added as a child to this node. def validate_child(node) case when !self.compatible_with(node) raise "cannot add child with class " + "#{node.class} to #{self.inspect}" when children.map(&:token).include?(node.token) raise 'cannot add child with occupied token: ' + node.token.inspect else true end end def compatible_with?(node) node.class == self.class end end