|
#--
|
|
# CyborgHood, a distributed system management software.
|
|
# Copyright (c) 2009-2011 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
|
|
|
|
include TaskAspect
|
|
|
|
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(list = nil, &block)
|
|
list ||= block
|
|
|
|
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
|
|
request.reply.results = list.nil? ? search_among_children(request, lookup_node) :
|
|
search_in_list(request, lookup_node, list)
|
|
end
|
|
request.send_reply
|
|
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, &send_result_cb)
|
|
args ||= []
|
|
raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments when calling node '#{node_path}'") unless args.is_a? Array
|
|
|
|
node = find_node(session, node_path)
|
|
raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node '#{node_path}'") if node.nil?
|
|
|
|
logger.debug "[Server API] Node '#{node_path}' found"
|
|
node.__send__(:request, session, args, &send_result_cb)
|
|
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 = [], &send_result_cb)
|
|
@session = session
|
|
@args = args
|
|
@send_result_cb = send_result_cb
|
|
|
|
@reply = {
|
|
:results => {},
|
|
:infos => [],
|
|
:warnings => [],
|
|
:errors => []
|
|
}.to_ostruct
|
|
end
|
|
|
|
def send_reply
|
|
@send_result_cb.call @reply.to_hash
|
|
end
|
|
end
|
|
|
|
def request(session, args = [], &send_result_cb)
|
|
request = Request.new(session, args, &send_result_cb)
|
|
|
|
if @request_cb
|
|
begin
|
|
@request_cb.call(request)
|
|
rescue
|
|
logger.debug "node request error message: " + $!
|
|
logger.debug "node request error backtrace: " + $!.backtrace.join("\n")
|
|
raise CyberError.new(:unrecoverable, 'api/cyborghood', "call failed on node '#{node_path}': " + $!)
|
|
end
|
|
else
|
|
request.reply.results = visible_nodes_names
|
|
request.send_reply
|
|
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
|
|
|
|
def search_among_children(request, lookup_node)
|
|
criterias = request.args.first
|
|
return {} unless criterias.is_a? Hash
|
|
|
|
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
|
|
|
|
child_node_attrs = result.to_hash
|
|
|
|
if hash_match_criterias(child_node_attrs, criterias)
|
|
node_names_list[child_node_name] = child_node_attrs
|
|
end
|
|
end
|
|
end
|
|
|
|
node_names_list
|
|
end
|
|
|
|
def search_in_list(request, lookup_node, list)
|
|
criterias = request.args.first
|
|
return {} unless criterias.is_a? Hash
|
|
|
|
if list.is_a? Proc
|
|
data = list.call(criterias)
|
|
|
|
return data[:list] unless data[:post_filter]
|
|
end
|
|
|
|
node_names_list = {}
|
|
|
|
data[:list].each do |element_name, element|
|
|
element_attrs = element.to_hash
|
|
|
|
if hash_match_criterias(element_attrs, criterias)
|
|
node_names_list[element_name] = element_attrs
|
|
end
|
|
end
|
|
|
|
node_names_list
|
|
end
|
|
end
|
|
end # DSL
|
|
end
|