# TODO: Investigate this kind of thing using Delegator to see if I can get # a speed boost. Each exported method would have to be dynamically defined # in the API class to do contract checking and then pass that on to the # delegated object. I don't, however, know if that's actually faster than # using method_missing. Investigate module Servicy class API def self.create(klass, method_descriptions) @klass = klass method_descriptions = { class: method_descriptions[:class].inject({}) { |h, info| h[info[:method].to_sym] = info; h }, instance: method_descriptions[:instance].inject({}) { |h, info| h[info[:method].to_sym] = info; h } } # We inherit from API here so that in the future we can add functionality # for setting up servers, service discovery, etc. # This may change to Client at some point... Who # knows. magic_class = Class.new(Servicy::API) do @@base_class = klass @@method_descriptions = method_descriptions def initialize(*args) @instance = @@base_class.new(*args) end def method_descriptions @@method_descriptions end # The magic happens in the two method_missing methods. # Meta-programming-averse; gaze upon my works, ye haughty, and despair! def method_missing(method, *args, &block) # Verify the contract data = method_descriptions[:instance][method] raise NoMethodError.new("#{@klass.to_s} does not expose an API endpoint called, #{method.to_s}") unless data data[:contract].valid_args?(*args) # Dispatch the result result = @instance.send(method, *args, &block) # Check the result data[:contract].valid_return?(*args, result) # And we are good to go result end # Make sure that we can respond to the things we actually do respond to. def respond_to?(method) method_descriptions[:instance].include?(method) || super end def self.method_missing(method, *args, &block) # Verify the contract data = @@method_descriptions[:class][method] raise NoMethodError.new("#{@klass.to_s} does not expose an API endpoint called, #{method.to_s}") unless data data[:contract].valid_args?(*args) # Dispatch the result result = @@base_class.send(method, *args, &block) # Check the result data[:contract].valid_return?(*args, result) # And we are good to go result end def self.respond_to?(method) @@method_descriptions[:class].include?(method) || super end # Send back either all the documentation, or just for a particular # method. def self.docs(method=nil) if method.nil? return @@method_descriptions.map do |(type, methods)| methods.values.map { |v| v[:docs] } end.flatten else search_in = [:class, :instance] if method.is_a?(String) search_in = method[0] == '.' ? [:class] : (method[0] == '#' ? [:instance] : search_in) end search_in.each do |type| @@method_descriptions[type].each do |(name,stuff)| return stuff[:docs] if method.to_s =~ /(\.|#)?#{name.to_s}$/ end end return nil end end end magic_class.extend(ExtraMethods) klass.constants(false).each do |const| magic_class.const_set(const, klass.const_get(const)) end # Give it a good name class_name = "#{klass.to_s}API" Object.const_set(class_name, magic_class) end # TODO: API wrappers that create a server of some kind to serve things up # TODO: A gem/library generator based on the API end module ExtraMethods # This method is used by the client discovery to build the query that can be # used to find remote instances of this service def search_query domain = self.const_get(:DOMAIN) rescue `hostname`.strip name = "#{domain}.#{self.to_s.downcase}" version = self.const_get(:VERSION) rescue '1.0.0' port = self.const_get(:PORT) rescue 1234 heartbeat_port = self.const_get(:HEARTBEAT_PORT) rescue port { name: name, host: 'localhost', port: port, version: version, heartbeat_port: heartbeat_port } end # Sets the remote configuration options def set_remote_configuration(config) @remote_config = config end end end