api.rb 9.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
module ActionWebService # :nodoc:
  module API # :nodoc:
    # A web service API class specifies the methods that will be available for
    # invocation for an API. It also contains metadata such as the method type
    # signature hints.
    #
    # It is not intended to be instantiated.
    #
    # It is attached to web service implementation classes like
    # ActionWebService::Base and ActionController::Base derivatives by using
L
Leon Breedt 已提交
11 12 13 14 15
    # <tt>container.web_service_api</tt>, where <tt>container</tt> is an
    # ActionController::Base or a ActionWebService::Base.
    #
    # See ActionWebService::Container::Direct::ClassMethods for an example
    # of use.
16
    class Base
17 18
      # Action WebService API subclasses should be reloaded by the dispatcher in Rails
      # when Dependencies.mechanism = :load.
19
      include Reloadable::Deprecated
20
      
21 22 23
      # Whether to transform the public API method names into camel-cased names 
      class_inheritable_option :inflect_names, true

24 25 26
      # By default only HTTP POST requests are processed
      class_inheritable_option :allowed_http_methods, [ :post ]
      
27
      # Whether to allow ActiveRecord::Base models in <tt>:expects</tt>.
28
      # The default is +false+; you should be aware of the security implications
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
      # of allowing this, and ensure that you don't allow remote callers to
      # easily overwrite data they should not have access to.
      class_inheritable_option :allow_active_record_expects, false

      # If present, the name of a method to call when the remote caller
      # tried to call a nonexistent method. Semantically equivalent to
      # +method_missing+.
      class_inheritable_option :default_api_method

      # Disallow instantiation
      private_class_method :new, :allocate
      
      class << self
        include ActionWebService::SignatureTypes

        # API methods have a +name+, which must be the Ruby method name to use when
        # performing the invocation on the web service object.
        #
        # The signatures for the method input parameters and return value can
        # by specified in +options+.
        #
        # A signature is an array of one or more parameter specifiers. 
        # A parameter specifier can be one of the following:
        #
53
        # * A symbol or string representing one of the Action Web Service base types.
L
Leon Breedt 已提交
54
        #   See ActionWebService::SignatureTypes for a canonical list of the base types.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
        # * The Class object of the parameter type
        # * A single-element Array containing one of the two preceding items. This
        #   will cause Action Web Service to treat the parameter at that position
        #   as an array containing only values of the given type.
        # * A Hash containing as key the name of the parameter, and as value
        #   one of the three preceding items
        # 
        # If no method input parameter or method return value signatures are given,
        # the method is assumed to take no parameters and/or return no values of
        # interest, and any values that are received by the server will be
        # discarded and ignored.
        #
        # Valid options:
        # [<tt>:expects</tt>]             Signature for the method input parameters
        # [<tt>:returns</tt>]             Signature for the method return value
        # [<tt>:expects_and_returns</tt>] Signature for both input parameters and return value
        def api_method(name, options={})
72 73 74
          unless options.is_a?(Hash)
            raise(ActionWebServiceError, "Expected a Hash for options")
          end
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
          validate_options([:expects, :returns, :expects_and_returns], options.keys)
          if options[:expects_and_returns]
            expects = options[:expects_and_returns]
            returns = options[:expects_and_returns]
          else
            expects = options[:expects]
            returns = options[:returns]
          end
          expects = canonical_signature(expects)
          returns = canonical_signature(returns)
          if expects
            expects.each do |type|
              type = type.element_type if type.is_a?(ArrayType)
              if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects
                raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects")
              end
            end
          end
          name = name.to_sym
          public_name = public_api_method_name(name)
          method = Method.new(name, public_name, expects, returns)
          write_inheritable_hash("api_methods", name => method)
          write_inheritable_hash("api_public_method_names", public_name => name)
        end

        # Whether the given method name is a service method on this API
        def has_api_method?(name)
          api_methods.has_key?(name)
        end
  
        # Whether the given public method name has a corresponding service method
        # on this API
        def has_public_api_method?(public_name)
          api_public_method_names.has_key?(public_name)
        end
  
        # The corresponding public method name for the given service method name
        def public_api_method_name(name)
          if inflect_names
            name.to_s.camelize
          else
            name.to_s
          end
        end
  
        # The corresponding service method name for the given public method name
        def api_method_name(public_name)
          api_public_method_names[public_name]
        end
  
        # A Hash containing all service methods on this API, and their
        # associated metadata.
        def api_methods
          read_inheritable_attribute("api_methods") || {}
        end

        # The Method instance for the given public API method name, if any
        def public_api_method_instance(public_method_name)
          api_method_instance(api_method_name(public_method_name))
        end

        # The Method instance for the given API method name, if any
        def api_method_instance(method_name)
          api_methods[method_name]
        end

        # The Method instance for the default API method, if any
        def default_api_method_instance
          return nil unless name = default_api_method
          instance = read_inheritable_attribute("default_api_method_instance")
          if instance && instance.name == name
            return instance
          end
          instance = Method.new(name, public_api_method_name(name), nil, nil)
          write_inheritable_attribute("default_api_method_instance", instance)
          instance
        end

        private
          def api_public_method_names
            read_inheritable_attribute("api_public_method_names") || {}
          end
  
          def validate_options(valid_option_keys, supplied_option_keys)
            unknown_option_keys = supplied_option_keys - valid_option_keys
            unless unknown_option_keys.empty?
              raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}")
            end
          end
      end
    end

    # Represents an API method and its associated metadata, and provides functionality
    # to assist in commonly performed API method tasks.
    class Method
      attr :name
      attr :public_name
      attr :expects
      attr :returns

      def initialize(name, public_name, expects, returns)
        @name = name
        @public_name = public_name
        @expects = expects
        @returns = returns
        @caster = ActionWebService::Casting::BaseCaster.new(self)
      end
      
      # The list of parameter names for this method
      def param_names
        return [] unless @expects
        @expects.map{ |type| type.name }
      end

      # Casts a set of Ruby values into the expected Ruby values
      def cast_expects(params)
        @caster.cast_expects(params)
      end

      # Cast a Ruby return value into the expected Ruby value
      def cast_returns(return_value)
        @caster.cast_returns(return_value)
      end

199 200 201 202 203 204 205 206 207 208
      # Returns the index of the first expected parameter
      # with the given name
      def expects_index_of(param_name)
        return -1 if @expects.nil?
        (0..(@expects.length-1)).each do |i|
          return i if @expects[i].name.to_s == param_name.to_s
        end
        -1
      end

209 210 211 212 213 214 215 216 217
      # Returns a hash keyed by parameter name for the given
      # parameter list
      def expects_to_hash(params)
        return {} if @expects.nil?
        h = {}
        @expects.zip(params){ |type, param| h[type.name] = param }
        h
      end

218 219 220 221 222 223 224 225 226 227
      # Backwards compatibility with previous API
      def [](sig_type)
        case sig_type
        when :expects
          @expects.map{|x| compat_signature_entry(x)}
        when :returns
          @returns.map{|x| compat_signature_entry(x)}
        end
      end

228 229 230
      # String representation of this method
      def to_s
        fqn = ""
231
        fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ")
232
        fqn << "#{@public_name}("
233
        fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects
234 235 236 237 238
        fqn << ")"
        fqn
      end

      private
239 240 241 242 243 244 245 246 247 248 249
        def compat_signature_entry(entry)
          if entry.array?
            [compat_signature_entry(entry.element_type)]
          else
            if entry.spec.is_a?(Hash)
              {entry.spec.keys.first => entry.type_class}
            else
              entry.type_class
            end
          end
        end
250 251 252
    end
  end
end