Project

General

Profile

« Previous | Next » 

Revision 7466fc08

Added by Marc Dequènes over 13 years ago

  • ID 7466fc083799a22deae3d56779823c810028f280

[evol] server API tree reworked using a DSL (reply handling rework is WIP)

View differences:

lib/cyborghood/cyborg/botnet/interface.rb
#++
require 'singleton'
require 'ostruct'
module CyborgHood
# the base mixin (not intended to be used directly, but...)
module CyborgServerInterfaceBase
NODE_PATTERN = "((?:\/|(?:\/[a-zA-Z0-9._]+)+[?=]?))"
module DSL
class ServerApiNode < BaseDSL
attr_reader :bot
attr_accessor :bot
# needed for testing node existence
reveal :nil?
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
attr_accessor :exported_methods
attr_accessor :auto_export_public_instance_methods
def initialize(bot, parent_node = nil, &block)
@bot = bot
@parent_node = parent_node
# don't call super because we need defered loading
@blocks = [block]
def export_method(*syms)
syms = [syms] unless syms.is_a? Array
self.exported_methods ||= []
self.exported_methods += syms.collect{|m| m.to_s }
cleanup
end
def unexport_method(*syms)
syms = [syms] unless syms.is_a? Array
self.exported_methods ||= []
self.exported_methods -= syms.collect{|m| m.to_s }
def add_behavior(&block)
@blocks << block
end
def export_parent_methods
self.export_method *self.superclass.public_instance_methods(false)
# string, array (useful for aliases), or regex
# TODO: name validation
def node(match, &block)
child_node = self.class.new(@bot, self, &block)
if match.is_a? Array
match.each{|n| @nodes[n] = child_node}
else
@nodes[match] = child_node
end
end
def is_node?(node)
(node =~ Regexp.new(NODE_PATTERN)) ? true : false
def on_request(&cb)
@request_cb = cb
end
end
def initialize(*args)
super(*args)
@config = Config.instance
self.class.exported_methods ||= []
self.class.auto_export_public_instance_methods = true
end
# convenience method
def is_node?(node)
self.class.is_node?(node)
end
def _is_node?(session, node_path)
node = __send__(:find_node, session, node_path)
not node.nil?
end
def api_klasses
list = self.class.constants.collect do |c|
cc = self.class.const_get(c)
(cc.class == Class and cc.ancestors.include? CyborgHood::CyborgServerInterfaceBase) ? [c, cc] : nil
end.compact
Hash[list]
end
def _call(session, node_path, args = nil)
args ||= []
raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments") unless args.is_a? Array
def api_methods
methods = []
methods += self.class.public_instance_methods(false) if self.class.auto_export_public_instance_methods
methods -= ["initialize", "__destroy", "method_missing"]
methods &= self.methods
methods += self.class.exported_methods
end
node = find_node(session, node_path)
raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node") if node.nil?
def api_container_methods
[]
end
logger.debug "[Server API] Node '#{node_path}' found"
r = node.__send__(:request, session, args)
logger.debug "[Server API] reply for node '#{node_path}': " + r.inspect
r
end
def api_containers
(api_klasses.keys + api_container_methods).sort
end
protected
def api_leafs
(api_methods - api_container_methods).sort
end
def root?
@parent_node.nil?
end
def api_nodes
(api_klasses.keys + api_methods).sort
end
def load(node_element = '')
cleanup
@node_name = node_element
@blocks.each do |bl|
instance_eval &bl
end
end
def find_node_action(session, node_name)
node_name.gsub!(/^\//, "")
next_node_name, other_nodes_names = node_name.split('/', 2)
def find_node(session, node_path)
# it is a string argument when interface root is called, but a list of node elemnts later
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
next_node_klass = next_node_name.nil? ? self.class : api_klasses[next_node_name]
# inner class or method ?
if next_node_klass.nil?
# method is declared ?
if api_methods.include? next_node_name
# final node ?
if other_nodes_names.blank?
# cannot use method(), as this method may not exist at all (but still
# be usuable through metaprogramming
return lambda do |*args|
r = child_node(next_node_name, session, *args)
# dynamic tree construction: method may return a node
if r.is_a? CyborgHood::CyborgServerInterfaceBase
r.api_nodes
else
r
end
end
end
# not a container, leaving
return unless self.api_container_methods.include? next_node_name
next_node = child_node(next_node_name, session)
node_element = node_path.shift
logger.debug "[Server API] Looking for node element '#{node_element}'"
if node_element.nil?
return self
else
# unknown method
return
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
else
next_node = next_node_klass.instance
# final node ?
return next_node.method('api_nodes') if other_nodes_names.blank?
end
# search deeper
if next_node.is_a? CyborgHood::CyborgServerInterfaceBase
next_node.find_node_action(session, other_nodes_names)
else
# it is not a node, so there are no children
return
end
end
def child_node(next_node_name, session, *args)
args.unshift session if self.is_a? CyborgHood::CyborgServerStatefulInterface
self.send(next_node_name, *args)
end
def has_node?(cmd)
not find_node_action(nil, cmd).nil?
end
# preliminary incoming message handling
def call(session, cmd, data = nil)
action = find_node_action(session, cmd)
raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node") if action.nil?
data ||= []
raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments") unless data.is_a? Array
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
begin
action.call(*data)
rescue
logger.debug "node action error message: " + $!
logger.debug "node action error backtrace: " + $!.backtrace.join("\n")
raise CyberError.new(:unrecoverable, 'api/cyborghood', "method call failed: " + $!)
nil
end
end
end
# structural mixins
module CyborgServerInterface
def self.included(base)
base.class_eval("include CyborgServerInterfaceBase")
base.class_eval("include Singleton")
base.extend(ClassMethods)
end
class Request
attr_reader :session, :args, :reply
module ClassMethods
def dynamic_interface(&resource_generator)
class_eval do
class_inheritable_reader :resource_generator
def initialize(session, args)
@session = session
@args = args
def method_missing(method_name, *args)
node_name = method_name.to_s
if api_methods.include?(node_name)
self.resource_generator.call(node_name)
else
super
end
end
@reply = {
:results => {},
:infos => [],
:warnings => [],
:errors => []
}.to_ostruct
end
write_inheritable_attribute(:resource_generator, resource_generator)
end
end
end
module CyborgServerEmbeddedInterface
def self.included(base)
base.class_eval("include CyborgServerInterfaceBase")
base.export_parent_methods
end
end
module CyborgServerStatefulInterface
def self.included(base)
base.class_eval("include CyborgServerInterfaceBase")
base.class_eval("include Singleton")
base.extend(ClassMethods)
end
module ClassMethods
def stateful_dynamic_interface(resource_key_pattern, &resource_generator)
class_eval do
class_inheritable_reader :resource_key_pattern, :resource_generator
def method_missing(method_name, *args)
session = args.shift
node_name = method_name.to_s
if api_methods.include?(node_name)
resource_key = self.resource_key_pattern.gsub("#NODE#", node_name)
session.store.get(resource_key) { self.resource_generator.call(node_name) }
else
super
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
@nodes.keys.collect do |match|
if match.is_a? String
match
elsif match.is_a? Regexp
'/' + match.to_s + '/'
elsif match.is_a? Proc
match.call
end
end.compact.flatten
end
write_inheritable_attribute(:resource_key_pattern, resource_key_pattern)
write_inheritable_attribute(:resource_generator, resource_generator)
end
end
end
# additional mixin
module CyborgServerRootInterfaceAddon
API_VERSION = "0.1~"
def self.included(base)
list = self.public_instance_methods(false)
base.class_eval do
export_method *list
def cleanup
@nodes = {}
@request_cb = nil
end
end
def product_name
PRODUCT
end
def product_version
VERSION
end
def api_version
API_VERSION
end
def bot_name
@bot.name
end
end
end

Also available in: Unified diff