Jiri-Kremser/simple-websocket-vcr

View on GitHub
lib/simple_websocket_vcr/recordable_websocket_client.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require 'delegate'
require 'websocket-client-simple'
require 'base64'
require 'simple_websocket_vcr/errors'

module WebSocketVCR
  include Errors

  class RecordableWebsocketClient
    include EventEmitter

    attr_reader :live, :client
    attr_accessor :session, :open, :thread

    def initialize(cassette, real_client)
      fail NoCassetteError 'specify the cassette' unless cassette

      if cassette.recording?
        @live = true
        @client = real_client
      else
        @live = false
        @open = true
      end
      @session = cassette.next_session
    end

    def send(data, opt = { type: :text })
      _write(:send, data, opt)
    end

    def on(event, params = {}, &block)
      super(event, params, &block) unless @live
      _read(event, params, &block)
    end

    def emit(event, *data)
      @session.store(operation: 'read', event: event, data: data) if @live
      super(event, *data)
    end

    def open?
      if @live
        @client.open?
      else
        @open
      end
    end

    def close
      if @live
        @session.store(operation: 'write', event: 'close')
        @client.close
      else
        sleep 0.5 if @session.head && @session.head.event != 'close'
        @session.record_entries.size.times do
          record = @session.next
          _ensure_event('close', record.event)
          emit(record.event, record.data) if record.operation == 'read'
        end
        Thread.kill @thread if @thread
        @open = false
      end
    end

    private

    def _write(method, data, opt)
      text_data = opt[:type] == :text ? data.dup : Base64.encode64(data.dup)
      if @live
        @client.__send__(method, data, opt)
        @session.store(operation: 'write', event: method, data: text_data)
      else
        sleep 0.5 if @session.head.operation != 'write'
        record = @session.next
        _ensure_operation('write', record.operation)
        _ensure_data(text_data, record.data)
      end
    end

    def _read(event, params, &_block)
      if @live
        rec = @session
        @client.on(event, params) do |msg|
          params = { operation: 'read', event: event }
          unless msg.nil?
            data = msg.type.to_s == 'text' ? msg.data : Base64.encode64(msg.data)
            params.merge!(type: msg.type, data: data)
          end
          rec.store(params)
          yield(msg)
        end
      else
        wait_for_reads unless @thread && @thread.alive?
      end
    end

    def wait_for_reads
      @thread = Thread.new do
        # if the next recorded operation is a 'read', take all the reads until next write
        # and translate them to the events
        while @open && !@session.empty?
          begin
            if @session.head.operation == 'read'
              record = @session.next

              emit(record.event, parse_data(record))
              break if __events.empty?
            else
              sleep 0.1 # TODO: config
            end
          end
        end
      end
    end

    def parse_data(record)
      if (data = record.data)
        data = Base64.decode64(data) if record.type != 'text'
        data = ::WebSocket::Frame::Data.new(data)

        def data.data
          self
        end
      end
      data
    end

    def _take_first_read
      @session.delete_at(@session.index { |record| record.operation == 'read' } || @session.length)
    end

    def _ensure_operation(desired, actual)
      string = "Expected to '#{desired}' but the next operation in recording was '#{actual}'"
      fail string unless desired == actual
    end

    def _ensure_event(desired, actual)
      string = "Expected to '#{desired}' but the next event in recording was '#{actual}'"
      fail string unless desired == actual
    end

    def _ensure_data(desired, actual)
      string = "Expected data to be '#{desired}' but next data in recording was '#{actual}'"
      fail string unless desired == actual
    end
  end
end