lib/pio/open_flow/message.rb
# 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