scaffolding.rb 10.3 KB
Newer Older
1
require 'benchmark'
2
require 'pathname'
3 4 5

module ActionWebService
  module Scaffolding # :nodoc:
6 7 8
    class ScaffoldingError < ActionWebServiceError # :nodoc:
    end

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
    def self.append_features(base)
      super
      base.extend(ClassMethods)
    end

    # Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The
    # generated scaffold actions have default views to let you enter the method parameters and view the
    # results.
    #
    # Example:
    #
    #   class ApiController < ActionController
    #     web_service_scaffold :invoke
    #   end
    #
    # This example generates an +invoke+ action in the +ApiController+ that you can navigate to from
    # your browser, select the API method, enter its parameters, and perform the invocation.
    #
    # If you want to customize the default views, create the following views in "app/views":
    #
    # * <tt>action_name/methods.rhtml</tt>
    # * <tt>action_name/parameters.rhtml</tt>
    # * <tt>action_name/result.rhtml</tt>
    # * <tt>action_name/layout.rhtml</tt>
    #
    # Where <tt>action_name</tt> is the name of the action you gave to ClassMethods#web_service_scaffold.
    #
    # You can use the default views in <tt>RAILS_DIR/lib/action_web_service/templates/scaffolds</tt> as
    # a guide.
    module ClassMethods
      # Generates web service invocation scaffolding for the current controller. The given action name
      # can then be used as the entry point for invoking API methods from a web browser.
      def web_service_scaffold(action_name)
        add_template_helper(Helpers)
        module_eval <<-END, __FILE__, __LINE__
          def #{action_name}
45
            if request.method == :get
46 47
              setup_invocation_assigns
              render_invocation_scaffold 'methods'
48 49 50 51
            end
          end

          def #{action_name}_method_params
52
            if request.method == :get
53 54
              setup_invocation_assigns
              render_invocation_scaffold 'parameters'
55 56 57 58
            end
          end

          def #{action_name}_submit
59
            if request.method == :post
60
              setup_invocation_assigns
61
              protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap
62 63
              case protocol_name
              when :soap
64
                @protocol = Protocol::Soap::SoapProtocol.new
65
              when :xmlrpc
66
                @protocol = Protocol::XmlRpc::XmlRpcProtocol.new
67
              end
68
              @invocation_cgi = request.respond_to?(:cgi) ? request.cgi : nil
69
              bm = Benchmark.measure do
70
                @protocol.register_api(@scaffold_service.api)
71
                post_params = params['method_params'] ? params['method_params'].dup : nil
72 73 74 75 76 77
                params = []
                if @scaffold_method.expects
                  @scaffold_method.expects.length.times do |i|
                    params << post_params[i.to_s]
                  end
                end
78
                params = @scaffold_method.cast_expects(params)
79 80 81 82
                method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name)
                @method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects)
                new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml)
                prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name)
83
                @request = new_request
84
                if @scaffold_container.dispatching_mode != :direct
85
                  request.parameters['action'] = @scaffold_service.name
86
                end
87 88
                dispatch_web_service_request
                @method_response_xml = @response.body
89
                method_name, obj = @protocol.decode_response(@method_response_xml)
90
                return if handle_invocation_exception(obj)
91
                @method_return_value = @scaffold_method.cast_returns(obj)
92 93
              end
              @method_elapsed = bm.real
94
              add_instance_variables_to_assigns
95 96
              reset_invocation_response
              render_invocation_scaffold 'result'
97 98 99 100
            end
          end

          private
101
            def setup_invocation_assigns
102 103 104
              @scaffold_class = self.class
              @scaffold_action_name = "#{action_name}"
              @scaffold_container = WebServiceModel::Container.new(self)
105 106 107
              if params['service'] && params['method']
                @scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] }
                @scaffold_method = @scaffold_service.api_methods[params['method']]
108 109 110 111
              end
              add_instance_variables_to_assigns
            end

112
            def render_invocation_scaffold(action)
113 114 115 116 117 118 119
              customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
              default_template = scaffold_path(action)
              @content_for_layout = template_exists?(customized_template) ? @template.render_file(customized_template) : @template.render_file(default_template, false)
              self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout"))
            end

            def scaffold_path(template_name)
120
              File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
121
            end
122 123

            def reset_invocation_response
124
              template = response.template
125 126 127 128 129
              if @invocation_cgi
                @response = ::ActionController::CgiResponse.new(@invocation_cgi)
              else
                @response = ::ActionController::TestResponse.new
              end
130
              response.template = template
131 132
              @performed_render = false
            end
133 134 135 136 137 138 139 140 141

            def public_method_name(service_name, method_name)
              if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
                service_name + '.' + method_name
              else
                method_name
              end
            end

142 143
            def prepare_request(new_request, service_name, method_name)
              new_request.parameters.update(request.parameters)
144
              if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol)
145
                new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}"
146 147
              end
            end
148 149 150 151 152 153 154 155 156 157 158 159 160

            def handle_invocation_exception(obj)
              exception = nil
              if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception)
                exception = obj.detail.cause
              elsif obj.is_a?(XMLRPC::FaultException)
                exception = obj
              end
              return unless exception
              reset_invocation_response
              rescue_action(exception)
              true
            end
161 162 163 164 165
        END
      end
    end

    module Helpers # :nodoc:
166 167 168
      def method_parameter_input_fields(method, type, field_name_base)
        if type.array?
          return content_tag('em', "Typed array input fields not supported yet (#{type.name})")
169
        end
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
        if type.structured?
          parameters = ""
          type.each_member do |member_name, member_type|
            label = method_parameter_label(member_name, member_type)
            nested_content = method_parameter_input_fields(
              method,
              member_type,
              field_name_base + '[' + member_name.to_s + ']')
            if member_type.custom?
              parameters << content_tag('li', label)
              parameters << content_tag('ul', nested_content)
            else
              parameters << content_tag('li', label + ' ' + nested_content)
            end
          end
          content_tag('ul', parameters)
        else
          case type.type
          when :int
            text_field_tag field_name_base
          when :string
            text_field_tag field_name_base
          when :bool
193 194
            radio_button_tag(field_name_base, "true") + " True" +
            radio_button_tag(field_name_base, "false") + "False"
195 196 197 198 199 200 201
          when :float
            text_field_tag field_name_base
          when :time
            select_datetime Time.now, 'name' => field_name_base
          when :date
            select_date Date.today, 'name' => field_name_base
          end
202 203 204
        end
      end

205 206 207 208
      def method_parameter_label(name, type)
        name.to_s.capitalize + ' (' + type.human_name(false) + ')'
      end

209 210 211 212 213 214 215 216 217 218 219 220
      def service_method_list(service)
        action = @scaffold_action_name + '_method_params'
        methods = service.api_methods_full.map do |desc, name|
          content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name))
        end
        content_tag("ul", methods.join("\n"))
      end
    end

    module WebServiceModel # :nodoc:
      class Container # :nodoc:
        attr :services
221
        attr :dispatching_mode
222 223 224

        def initialize(real_container)
          @real_container = real_container
225
          @dispatching_mode = @real_container.class.web_service_dispatching_mode
226
          @services = []
227
          if @dispatching_mode == :direct
228 229
            @services << Service.new(@real_container.controller_name, @real_container)
          else
230
            @real_container.class.web_services.each do |name, obj|
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
              @services << Service.new(name, @real_container.instance_eval{ web_service_object(name) })
            end
          end
        end
      end

      class Service # :nodoc:
        attr :name
        attr :object
        attr :api
        attr :api_methods
        attr :api_methods_full

        def initialize(name, real_service)
          @name = name.to_s
          @object = real_service
          @api = @object.class.web_service_api
248 249 250
          if @api.nil?
            raise ScaffoldingError, "No web service API attached to #{object.class}"
          end
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
          @api_methods = {}
          @api_methods_full = []
          @api.api_methods.each do |name, method|
            @api_methods[method.public_name.to_s] = method
            @api_methods_full << [method.to_s, method.public_name.to_s]
          end
        end

        def to_s
          self.name.camelize
        end
      end
    end
  end
end