scaffolding.rb 11.2 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
    def self.included(base)
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
      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)
42
        module_eval <<-"end_eval", __FILE__, __LINE__
43
          def #{action_name}
44
            if request.method == :get
45 46
              setup_invocation_assigns
              render_invocation_scaffold 'methods'
47 48 49 50
            end
          end

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

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

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

108
            def render_invocation_scaffold(action)
109 110
              customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
              default_template = scaffold_path(action)
111 112 113 114 115 116 117 118 119 120 121
              if template_exists?(customized_template)
                content = @template.render_file(customized_template)
              else
                content = @template.render_file(default_template, false)
              end
              @template.instance_variable_set("@content_for_layout", content)
              if self.active_layout.nil?
                render_file(scaffold_path("layout"))
              else
                render_file(self.active_layout, "200 OK", true)
              end
122 123 124
            end

            def scaffold_path(template_name)
125
              File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
126
            end
127 128

            def reset_invocation_response
129 130
              erase_render_results
              @response.headers = ::ActionController::AbstractResponse::DEFAULT_HEADERS.merge("cookie" => [])
131
            end
132 133 134 135 136 137 138 139 140

            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

141 142
            def prepare_request(new_request, service_name, method_name)
              new_request.parameters.update(request.parameters)
143
              request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) }
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
        end_eval
162 163 164 165
      end
    end

    module Helpers # :nodoc:
166
      def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false)
167 168
        if type.array?
          return content_tag('em', "Typed array input fields not supported yet (#{type.name})")
169
        end
170
        if type.structured?
171
          return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured
172 173 174 175 176 177
          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,
178
              "#{field_name_base}[#{idx}][#{member_name}]",
179 180
              idx,
              true)
181 182 183 184 185 186 187 188 189
            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
190 191 192
          # If the data source was structured previously we already have the index set          
          field_name_base = "#{field_name_base}[#{idx}]" unless was_structured
          
193 194
          case type.type
          when :int
195
            text_field_tag "#{field_name_base}"
196
          when :string
197
            text_field_tag "#{field_name_base}"
198
          when :base64
199
            text_area_tag "#{field_name_base}", nil, :size => "40x5"
200
          when :bool
201 202
            radio_button_tag("#{field_name_base}", "true") + " True" +
            radio_button_tag("#{field_name_base}", "false") + "False"
203
          when :float
204
            text_field_tag "#{field_name_base}"
205 206 207 208 209
          when :time, :datetime
            time = Time.now
            i = 0
            %w|year month day hour minute second|.map do |name|
              i += 1
210
              send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
211
            end.join
212
          when :date
213 214 215 216
            date = Date.today
            i = 0
            %w|year month day|.map do |name|
              i += 1
217
              send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
218
            end.join
219
          end
220 221 222
        end
      end

223 224 225 226
      def method_parameter_label(name, type)
        name.to_s.capitalize + ' (' + type.human_name(false) + ')'
      end

227 228 229 230 231 232 233 234 235 236 237 238
      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
239
        attr :dispatching_mode
240 241 242

        def initialize(real_container)
          @real_container = real_container
243
          @dispatching_mode = @real_container.class.web_service_dispatching_mode
244
          @services = []
245
          if @dispatching_mode == :direct
246 247
            @services << Service.new(@real_container.controller_name, @real_container)
          else
248
            @real_container.class.web_services.each do |name, obj|
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
              @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
266 267 268
          if @api.nil?
            raise ScaffoldingError, "No web service API attached to #{object.class}"
          end
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
          @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