|
#--
|
|
# CyborgHood, a distributed system management software.
|
|
# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#++
|
|
|
|
require 'singleton'
|
|
require 'ostruct'
|
|
|
|
|
|
module CyborgHood
|
|
module DSL
|
|
class ServerApiNode < BaseDSL
|
|
attr_reader :bot, :node_name, :parent_node, :store
|
|
|
|
# needed for testing node existence
|
|
reveal :nil?
|
|
reveal :respond_to?
|
|
|
|
def initialize(bot, parent_node = nil, options = {}, &block)
|
|
@bot = bot
|
|
@parent_node = parent_node
|
|
@ldir = options[:dir]
|
|
@hidden = options[:hidden] || false
|
|
|
|
# don't call super because we need defered loading
|
|
|
|
@blocks = []
|
|
add_behavior(&block)
|
|
|
|
@ldir_loaded = false
|
|
|
|
cleanup
|
|
end
|
|
|
|
def root?
|
|
@parent_node.nil?
|
|
end
|
|
|
|
def hidden?
|
|
@hidden
|
|
end
|
|
|
|
def node_path
|
|
return '/' if root?
|
|
|
|
path = @parent_node.node_path
|
|
path += "/" + @node_name unless @node_name.empty?
|
|
path.gsub("//", "/")
|
|
end
|
|
|
|
def add_behavior(&block)
|
|
if block_given?
|
|
@blocks << block
|
|
else
|
|
return if @ldir_loaded
|
|
|
|
begin
|
|
Dir.glob(File.join(self.lpath, '*.rb')) do |file|
|
|
logger.debug "Interface: loading file '#{file}'"
|
|
begin
|
|
@blocks << File.read(file)
|
|
rescue
|
|
logger.error "Interface: definition file '#{file}' cannot be read"
|
|
end
|
|
end
|
|
rescue
|
|
logger.error "Interface: directory '#{self.lpath}' cannot be read"
|
|
end
|
|
|
|
@ldir_loaded = true
|
|
end
|
|
end
|
|
|
|
# string, array (useful for aliases), or regex
|
|
# TODO: name validation
|
|
def node(match, options = {}, &block)
|
|
child_node = self.class.new(@bot, self, options, &block)
|
|
if match.is_a? Array
|
|
match.each{|n| @nodes[n] = child_node}
|
|
else
|
|
@nodes[match] = child_node
|
|
end
|
|
end
|
|
|
|
def attr_search_node
|
|
lookup_node = self
|
|
|
|
node '?', :hidden => true do
|
|
on_request do |request|
|
|
if request.args.empty?
|
|
request.reply.results = lookup_node.__send__(:visible_nodes_names)
|
|
else
|
|
node_names_list = []
|
|
|
|
lookup_node.__send__(:visible_nodes).each do |match, node|
|
|
if match.is_a? String
|
|
match_list = [match]
|
|
elsif match.is_a? Proc
|
|
match_list = match.call
|
|
else
|
|
next
|
|
end
|
|
|
|
match_list.each do |child_node_name|
|
|
node.__send__(:load, child_node_name)
|
|
result = node.__send__(:request, request.session)
|
|
next unless result.respond_to? :to_hash
|
|
|
|
if hash_match_criterias(result.to_hash, request.args.first)
|
|
node_names_list << child_node_name
|
|
end
|
|
end
|
|
end
|
|
|
|
request.reply.results = node_names_list
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def on_request(&cb)
|
|
@request_cb = cb
|
|
end
|
|
|
|
def _is_node?(session, node_path)
|
|
node = __send__(:find_node, session, node_path)
|
|
not node.nil?
|
|
end
|
|
|
|
def _call(session, node_path, args = nil)
|
|
args ||= []
|
|
raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments") unless args.is_a? Array
|
|
|
|
node = find_node(session, node_path)
|
|
raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node") if node.nil?
|
|
|
|
logger.debug "[Server API] Node '#{node_path}' found"
|
|
node.__send__(:request, session, args)
|
|
end
|
|
|
|
protected
|
|
|
|
def base_lpath
|
|
File.join(Config::LIB_DIR, 'cyborghood-' + @bot.name.downcase, 'interface')
|
|
end
|
|
|
|
def lpath
|
|
parent_lpath = @parent_node.nil? ? self.base_lpath : @parent_node.lpath
|
|
@ldir.nil? ? parent_lpath : File.join(parent_lpath, @ldir)
|
|
end
|
|
|
|
def load(node_element = '')
|
|
cleanup
|
|
@node_name = node_element
|
|
@blocks.each do |bl|
|
|
begin
|
|
if bl.is_a? String
|
|
instance_eval bl
|
|
else
|
|
instance_eval &bl
|
|
end
|
|
rescue Exception => e
|
|
logger.error "Interface: a definition block for node '#{node_path}' is buggy: " + e.message
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_node(session, node_path)
|
|
# node_path is a string argument when interface root node is called, but is a list of node elements later on
|
|
if root?
|
|
logger.debug "[Server API] Looking for node '#{node_path}'"
|
|
node_path = node_path.split("/")
|
|
# remove empty string before first "/"
|
|
node_path.shift
|
|
# initial load
|
|
load
|
|
end
|
|
|
|
node_element = node_path.shift
|
|
logger.debug "[Server API] Looking for node element '#{node_element}'"
|
|
if node_element.nil?
|
|
return self
|
|
else
|
|
next_node = find_child_node(node_element)
|
|
if next_node
|
|
next_node.__send__(:load, node_element)
|
|
return next_node.__send__(:find_node, session, node_path)
|
|
else
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_child_node(child_node)
|
|
return @nodes[child_node] if @nodes.has_key? child_node
|
|
|
|
@nodes.each_pair do |match, node|
|
|
found = if match.is_a? String
|
|
child_node == match
|
|
elsif match.is_a? Regexp
|
|
child_node =~ Regexp.new(match)
|
|
elsif match.is_a? Proc
|
|
match.call.include? child_node
|
|
end
|
|
return node if found
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
class Request
|
|
attr_reader :session, :args, :reply
|
|
|
|
def initialize(session, args = [])
|
|
@session = session
|
|
@args = args
|
|
|
|
@reply = {
|
|
:results => {},
|
|
:infos => [],
|
|
:warnings => [],
|
|
:errors => []
|
|
}.to_ostruct
|
|
end
|
|
end
|
|
|
|
def request(session, args = [])
|
|
if @request_cb
|
|
request = Request.new(session, args)
|
|
begin
|
|
@request_cb.call(request)
|
|
# TODO: full reply needed
|
|
request.reply.results
|
|
rescue
|
|
logger.debug "node request error message: " + $!
|
|
logger.debug "node request error backtrace: " + $!.backtrace.join("\n")
|
|
raise CyberError.new(:unrecoverable, 'api/cyborghood', "method call failed: " + $!)
|
|
end
|
|
else
|
|
visible_nodes_names
|
|
end
|
|
end
|
|
|
|
def visible_nodes
|
|
Hash[@nodes.select{|match, node| not node.hidden? }]
|
|
end
|
|
|
|
def node_match_to_name(match)
|
|
if match.is_a? String
|
|
match
|
|
elsif match.is_a? Regexp
|
|
'/' + match.to_s + '/'
|
|
elsif match.is_a? Proc
|
|
match.call
|
|
end
|
|
end
|
|
|
|
def visible_nodes_names
|
|
visible_nodes.keys.collect do |match|
|
|
node_match_to_name(match)
|
|
end.compact.flatten
|
|
end
|
|
|
|
def cleanup
|
|
@nodes = {}
|
|
@request_cb = nil
|
|
|
|
# data memorized during walk in the node tree
|
|
if root?
|
|
@store = OpenStruct.new
|
|
else
|
|
@store = @parent_node.store
|
|
end
|
|
end
|
|
|
|
def hash_match_criterias(hash, crit)
|
|
crit.each do |key, wanted_value|
|
|
value = hash[key]
|
|
|
|
if wanted_value.is_a? Array
|
|
return false unless wanted_value.include?(value)
|
|
elsif wanted_value.is_a? Regexp
|
|
return false unless value =~ wanted_value
|
|
else
|
|
return false unless value == wanted_value
|
|
end
|
|
end
|
|
|
|
true
|
|
end
|
|
end
|
|
end
|
|
end
|