Class: Servicy::API

Inherits:
Object
  • Object
show all
Defined in:
lib/api.rb

Class Method Summary collapse

Class Method Details

.create(klass, mds) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/api.rb', line 3

def self.create(klass, mds)
  method_descriptions = { class:    mds[:class].inject({}) { |h, info| h[info[:method].to_sym] = info; h },
                          instance: mds[: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
    def initialize(*args)
      @instance = self.class.const_get('BASE_CLASS').new(*args)
    end

    def self.set_remote(client)
      @remote = client
    end
    def self.remote?
      @remote
    end
    def self.client
      @remote
    end

    # TODO: I'm thinking that I might be able to do this with
    # #define_method's instead. That will spead things up aroud 2-5x.

    # 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 = self.class.const_get('METHOD_DESCRIPTIONS')[:instance][method]
      raise NoMethodError.new("#{self.class.const_get('BASE_CLASS')} does not expose an API endpoint called, #{method.to_s}") unless data
      data[:contract].valid_args?(*args)

      # Dispatch the result
      result = nil
      if self.class.remote?
        params = data[:contract].params.each_with_index.inject({}) do |h, (param, index)|
          h[param.to_s] = args[index]
          h
        end
        result = self.class.client.transport.remote_request(method, params)
      else
        result = @instance.send(method, *args, &block)
      end

      # 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)
      self.class.const_get('METHOD_DESCRIPTIONS')[:instance].include?(method) || super
    end

    def self.method_missing(method, *args, &block)
      # Verify the contract
      data = self.const_get('METHOD_DESCRIPTIONS')[:class][method]
      raise NoMethodError.new("#{self.const_get('BASE_CLASS')} does not expose an API endpoint called, #{method.to_s}") unless data
      data[:contract].valid_args?(*args)

      # Dispatch the result
      result = nil
      if remote?
        params = data[:contract].params.each_with_index.inject({}) do |h, (param, index)|
          h[param.to_s] = args[index]
          h
        end
        result = client.transport.remote_request(method, params)
      else
        result = self.const_get('BASE_CLASS').send(method, *args, &block)
      end

      # Check the result
      data[:contract].valid_return?(*args, result)

      # And we are good to go
      result
    end

    def self.respond_to?(method)
      const_get('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 const_get('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|
          const_get('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
  magic_class.const_set('BASE_CLASS', klass)
  magic_class.const_set('METHOD_DESCRIPTIONS', method_descriptions)

  # Give it a good name
  class_name = "#{klass.to_s}API"
  Object.const_set(class_name, magic_class)
end