lib/pio/open_flow/message.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/descendants_tracker'
require 'bindata'
require 'pio/open_flow/flags'
require 'pio/open_flow/header'
require 'pio/parse_error'
require 'pio/ruby_dumper'

module Pio
  module OpenFlow
    # OpenFlow messages.
    class Message
      attr_reader :format

      extend ActiveSupport::DescendantsTracker
      extend OpenFlow::Flags

      def self.read(raw_data)
        allocate.tap do |message|
          message.instance_variable_set(:@format,
                                        const_get(:Format).read(raw_data))
        end
      rescue BinData::ValidityError
        message_name = name.split('::')[1..-1].join(' ')
        raise Pio::ParseError, "Invalid #{message_name} message."
      end

      # rubocop:disable MethodLength
      # rubocop:disable AbcSize
      def self.method_missing(method, *args, &block)
        begin
          const_get(:Format).__send__ method, *args, &block
        rescue NameError
          klass = Class.new(BinData::Record)
          const_set :Format, klass
          klass.class_eval do
            include RubyDumper
            define_method(:header_length) { 8 }
            define_method(:length) { _length }
          end
          class_variable_set(:@@valid_options, [])
          retry
        end

        return if method == :endian || method == :virtual

        define_method(args.first) do
          snapshot = @format.snapshot.__send__(args.first)
          if snapshot.class == BinData::Struct::Snapshot
            @format.__send__(args.first)
          else
            snapshot
          end
        end
        class_variable_set(:@@valid_options,
                           class_variable_get(:@@valid_options) + [args.first])
      end
      # rubocop:enable MethodLength
      # rubocop:enable AbcSize

      # rubocop:disable AbcSize
      # rubocop:disable MethodLength
      def self.open_flow_header(opts)
        module_eval do
          cattr_reader(:type) { opts.fetch(:type) }

          endian :big

          uint8 :version, value: opts.fetch(:version)
          uint8 :type, value: opts.fetch(:type)
          uint16(:_length,
                 initial_value: opts[:length] || lambda do
                   begin
                     8 + body.length
                   rescue
                     8
                   end
                 end)
          transaction_id :transaction_id, initial_value: 0

          virtual assert: -> { version == opts.fetch(:version) }
          virtual assert: -> { type == opts.fetch(:type) }

          alias_method :xid, :transaction_id
        end
      end
      # rubocop:enable AbcSize
      # rubocop:enable MethodLength

      def initialize(user_options = {})
        validate_user_options user_options
        @format = self.class.const_get(:Format).new(parse_options(user_options))
      end

      def to_binary
        @format.to_binary_s
      end

      def method_missing(method, *args, &block)
        @format.__send__ method, *args, &block
      end

      private

      def validate_user_options(user_options)
        unknown_options =
          user_options.keys - self.class.class_variable_get(:@@valid_options)
        return if unknown_options.empty?
        raise "Unknown option: #{unknown_options.first}"
      end

      def parse_options(user_options)
        parsed_options = user_options.dup
        parsed_options[:transaction_id] = user_options[:transaction_id] || 0
        parsed_options[:body] = user_options[:body] || ''
        parsed_options
      end
    end
  end
end