<commit_before># encoding: UTF-8

require 'forwardable'

module Shapewear::Request
  # @param request [Rack::Request, Hash]
  def serve(request)
    RequestHandler.new(self, request).serve
  end

  class RequestHandler
    extend Forwardable

    attr_reader :soap_version, :op_node, :clazz
    def_delegators :@clazz, :namespaces, :operations, :logger

    def initialize(clazz, request)
      @clazz = clazz

      body ||= request.body if request.respond_to? :body
      body ||= request[:body] if request.is_a?(Hash)
      body ||= request.to_s

      raise "Request body could not be found" if body.nil?

      doc = Nokogiri::XML(body) { |c| c.strict } rescue raise("Request body is not a valid XML")

      # detect the SOAP version from the envelope, and find the operation element, or raise if not found
      if doc.at('/env:Envelope', namespaces)
        @soap_version = :soap11
        @op_node = doc.at("/env:Envelope/env:Body/tns:*", namespaces)
      elsif doc.at('/env12:Envelope', namespaces)
        @soap_version = :soap12
        @op_node = doc.at("/env12:Envelope/env12:Body/tns:*", namespaces)
      else
        raise "Request is not a SOAP 1.1 nor SOAP 1.2 Envelope: #{body}"
      end
    end

    def serve
      call_soap_operation
    end

    private

    def call_soap_operation
      raise "Operation node not found" if op_node.nil?

      operations.each do |k, v|
        if v[:public_name] == op_node.name
          logger.debug "Extracting parameters from operation node..."
          params = extract_parameters(v, op_node)
          logger.debug "Creating new instance of #{clazz}..."
          obj = clazz.new
          logger.debug "Calling #{k} with args: #{params.map(&:inspect) * ', '}"
          begin
            r = obj.send(k, *params)
            logger.debug "Result: #{r.inspect}"
            return serialize_soap_result v, r
          rescue => e
            logger.debug "Exception: #{e.inspect}"
            return serialize_soap_fault e
          end
        end
      end

      raise "Operation not found: #{@op_node.name}"
    end

    def extract_parameters(op_options, node)
      logger.debug "Operation node: #{node.inspect}"
      r = []
      op_options[:parameters].each do |p|
        logger.debug "  Looking for: tns:#{p.first.camelize_if_symbol(:lower)}"
        v = node.xpath("tns:#{p.first.camelize_if_symbol(:lower)}", namespaces).first
        if v.nil?
          # does nothing
        elsif p.last == Fixnum
          v = v.text.to_i
        elsif p.last == DateTime
          v = DateTime.parse(v.text) # TODO: add tests
        else
          v = v.text
        end
        logger.debug "    Found: #{v.inspect}"
        r << v
      end
      r
    end

    #noinspection RubyArgCount
    def serialize_soap_result(op_options, r)
      xb = Builder::XmlMarkup.new
      xb.instruct!

      xb.Envelope :xmlns => soap_env_ns, 'xmlns:xsi' => namespaces['xsi'] do |xenv|
        xenv.Body do |xbody|
          xbody.tag! "#{op_options[:public_name]}Response", :xmlns => namespaces['tns'] do |xresp|

            if r.nil?
              xresp.tag! "#{op_options[:public_name]}Result", 'xsi:nil' => 'true'
            else
              ret = op_options[:returns] rescue nil
              case ret
                when NilClass, Class
                  xresp.tag! "#{op_options[:public_name]}Result", r
                when Hash
                  xresp.tag! "#{op_options[:public_name]}Result" do |xres|
                    ret.each do |k, v|
                      extract_and_serialize_value(xres, r, k, v)
                    end
                  end
                else
                  raise "Unsupported return type: #{ret.inspect}"
              end
            end
          end
        end
      end
    end

    def extract_and_serialize_value(builder, obj, field, type)
      v = if obj.is_a?(Hash)
        obj[field]
      elsif obj.respond_to?(field)
        obj.send(field)
      elsif obj.respond_to?(field.underscore)
        obj.send(field.underscore)
      else
        raise "Could not extract #{field.inspect} from object: #{obj.inspect}"
      end

      if v.nil?
        builder.tag! field.camelize_if_symbol, 'xsi:nil' => 'true'
      else
        builder.tag! field.camelize_if_symbol, v
      end
    end

    #noinspection RubyArgCount
    def serialize_soap_fault(ex)
      logger.debug "Serializing SOAP Fault: #{ex.inspect}"

      xb = Builder::XmlMarkup.new
      xb.instruct!

      xb.tag! 'e:Envelope', 'xmlns:e' => soap_env_ns do |xenv|
        xenv.tag! 'e:Body' do |xbody|
          xbody.tag! 'e:Fault' do |xf|
            case soap_version
              when :soap11
                xf.faultcode "e:Server.#{ex.class.name}"
                xf.faultstring ex.message
              when :soap12
                xf.tag! 'e:Code' do |xcode|
                  xcode.tag! 'e:Value', 'e:Receiver'
                  xcode.tag! 'e:Subcode' do |xsubcode|
                    xsubcode.tag! 'e:Value', ex.class.name
                  end
                end
                xf.tag! 'e:Reason', ex.message
              else
                raise "Unsupported SOAP version: #{soap_version}"
            end
          end
        end
      end
    end

    def soap_env_ns
      case soap_version
        when :soap11
          namespaces['env']
        when :soap12
          namespaces['env12']
        else
          raise "Unrecognized SOAP version: #{soap_version}"
      end
    end
  end
end<commit_msg>Better key lookup on hashes.<commit_after># encoding: UTF-8

require 'forwardable'

module Shapewear::Request
  # @param request [Rack::Request, Hash]
  def serve(request)
    RequestHandler.new(self, request).serve
  end

  class RequestHandler
    extend Forwardable

    attr_reader :soap_version, :op_node, :clazz
    def_delegators :@clazz, :namespaces, :operations, :logger

    def initialize(clazz, request)
      @clazz = clazz

      body ||= request.body if request.respond_to? :body
      body ||= request[:body] if request.is_a?(Hash)
      body ||= request.to_s

      raise "Request body could not be found" if body.nil?

      doc = Nokogiri::XML(body) { |c| c.strict } rescue raise("Request body is not a valid XML")

      # detect the SOAP version from the envelope, and find the operation element, or raise if not found
      if doc.at('/env:Envelope', namespaces)
        @soap_version = :soap11
        @op_node = doc.at("/env:Envelope/env:Body/tns:*", namespaces)
      elsif doc.at('/env12:Envelope', namespaces)
        @soap_version = :soap12
        @op_node = doc.at("/env12:Envelope/env12:Body/tns:*", namespaces)
      else
        raise "Request is not a SOAP 1.1 nor SOAP 1.2 Envelope: #{body}"
      end
    end

    def serve
      call_soap_operation
    end

    private

    def call_soap_operation
      raise "Operation node not found" if op_node.nil?

      operations.each do |k, v|
        if v[:public_name] == op_node.name
          logger.debug "Extracting parameters from operation node..."
          params = extract_parameters(v, op_node)
          logger.debug "Creating new instance of #{clazz}..."
          obj = clazz.new
          logger.debug "Calling #{k} with args: #{params.map(&:inspect) * ', '}"
          begin
            r = obj.send(k, *params)
            logger.debug "Result: #{r.inspect}"
            return serialize_soap_result v, r
          rescue => e
            logger.debug "Exception: #{e.inspect}"
            return serialize_soap_fault e
          end
        end
      end

      raise "Operation not found: #{@op_node.name}"
    end

    # Extracts all parameters from the operation node, and return as an array.
    #
    # @param op_options [Hash] The operation options.
    # @param node [Nokogiri::XML] The operation node.
    # @return [Array] The parsed parameters.
    def extract_parameters(op_options, node)
      logger.debug "Operation node: #{node.inspect}"
      r = []
      op_options[:parameters].each do |p|
        logger.debug "  Looking for: tns:#{p.first.camelize_if_symbol(:lower)}"
        v = node.xpath("tns:#{p.first.camelize_if_symbol(:lower)}", namespaces).first
        if v.nil?
          # does nothing
        elsif p.last == Fixnum
          v = v.text.to_i
        elsif p.last == DateTime
          v = DateTime.parse(v.text) # TODO: add tests
        else
          v = v.text
        end
        logger.debug "    Found: #{v.inspect}"
        r << v
      end
      r
    end

    # Serializes the result of an operation as a SOAP Envelope.
    #
    # @param op_options [Hash] The operation options.
    # @param r [Hash,Object] The operation result.
    #noinspection RubyArgCount
    def serialize_soap_result(op_options, r)
      xb = Builder::XmlMarkup.new
      xb.instruct!

      xb.Envelope :xmlns => soap_env_ns, 'xmlns:xsi' => namespaces['xsi'] do |xenv|
        xenv.Body do |xbody|
          xbody.tag! "#{op_options[:public_name]}Response", :xmlns => namespaces['tns'] do |xresp|
            if r.nil?
              xresp.tag! "#{op_options[:public_name]}Result", 'xsi:nil' => 'true'
            else
              ret = op_options[:returns] rescue nil
              case ret
                when NilClass, Class
                  xresp.tag! "#{op_options[:public_name]}Result", r
                when Hash
                  xresp.tag! "#{op_options[:public_name]}Result" do |xres|
                    ret.each do |k, v|
                      extract_and_serialize_value(xres, r, k, v)
                    end
                  end
                else
                  raise "Unsupported return type: #{ret.inspect}"
              end
            end
          end
        end
      end
    end

    # Extracts a field from an object, casts it to the appropriate type, and serializes as XML.
    #
    # @param builder [Builder::XmlMarkup] The XML builder.
    # @param obj [Hash,Object] The resulting object.
    # @param field [Symbol,String] The field to extract.
    # @param type [Class] The type to convert.
    def extract_and_serialize_value(builder, obj, field, type)
      v = if obj.is_a?(Hash)
        obj[field] or obj[field.underscore]
      elsif obj.respond_to?(field)
        obj.send(field)
      elsif obj.respond_to?(field.underscore)
        obj.send(field.underscore)
      else
        raise "Could not extract #{field.inspect} from object: #{obj.inspect}"
      end

      if v.nil?
        builder.tag! field.camelize_if_symbol, 'xsi:nil' => 'true'
      else
        builder.tag! field.camelize_if_symbol, v
      end
    end

    # Serializes an exception as a SOAP Envelope containing a SOAP Fault.
    #
    # @param ex [Exception] The Exception to serialize.
    # @return [String] The SOAP Envelope containing the Fault.
    #noinspection RubyArgCount
    def serialize_soap_fault(ex)
      logger.debug "Serializing SOAP Fault: #{ex.inspect}"

      xb = Builder::XmlMarkup.new
      xb.instruct!

      xb.tag! 'e:Envelope', 'xmlns:e' => soap_env_ns do |xenv|
        xenv.tag! 'e:Body' do |xbody|
          xbody.tag! 'e:Fault' do |xf|
            case soap_version
              when :soap11
                xf.faultcode "e:Server.#{ex.class.name}"
                xf.faultstring ex.message
              when :soap12
                xf.tag! 'e:Code' do |xcode|
                  xcode.tag! 'e:Value', 'e:Receiver'
                  xcode.tag! 'e:Subcode' do |xsubcode|
                    xsubcode.tag! 'e:Value', ex.class.name
                  end
                end
                xf.tag! 'e:Reason', ex.message
              else
                raise "Unsupported SOAP version: #{soap_version}"
            end
          end
        end
      end
    end

    # Returns the correct SOAP Envelope namespace.
    #
    # @return [String] The correct SOAP Envelope namespace.
    def soap_env_ns
      case soap_version
        when :soap11
          namespaces['env']
        when :soap12
          namespaces['env12']
        else
          raise "Unrecognized SOAP version: #{soap_version}"
      end
    end
  end
end
{'fraction_non_alphanumeric': 0.10631092020485709, 'fraction_numerical': 0.004130183380142078, 'mean_word_length': 1.918052542781393, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1426948', 'n_tokens_mistral': 3812, 'n_tokens_neox': 3671, 'n_words': 991}