module Nmspec::Ruby

Public Class Methods

_bool_type() click to toggle source

inserts the boolean type readers and writers

# File lib/nmspec/ruby.rb, line 117
def _bool_type
  code = []

  code << '  ###########################################'
  code << '  # boolean type'
  code << '  ###########################################'
  code << ''
  code << "  def r_bool"
  code << "    @socket.recv(1).unpack('C')[0] == 1"
  code << '  end'
  code << ''
  code << "  def w_bool(bool)"
  code << "    @socket.send([bool ? 1 : 0].pack('C'), 0)"
  code << '  end'

  code
end
_class_name_from_msgr_name(name) click to toggle source
# File lib/nmspec/ruby.rb, line 50
def _class_name_from_msgr_name(name)
  name
    .downcase
    .gsub(/[\._\-]/, ' ')
    .split(' ')
    .map{|part| part.capitalize}
    .join + 'Msgr'
end
_close() click to toggle source
# File lib/nmspec/ruby.rb, line 86
def _close
  code = []

  code << '  ##'
  code << '  # closes the socket inside this object'
  code << '  def close'
  code << '    @socket&.close'
  code << '  end'

  code
end
_flip_mode(msg) click to toggle source
# File lib/nmspec/ruby.rb, line 382
def _flip_mode(msg)
  opposite_mode = msg[:mode] == :read ? :write : :read
  { mode: opposite_mode, type: msg[:type], identifier: msg[:identifier] }
end
_initialize() click to toggle source
# File lib/nmspec/ruby.rb, line 75
def _initialize
  code = []

  code << '  def initialize(socket, no_delay=false)'
  code << '    @socket = socket'
  code << '    @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if no_delay'
  code << '  end'

  code
end
_line_from_msg(msg) click to toggle source
# File lib/nmspec/ruby.rb, line 387
def _line_from_msg(msg)
  mode = msg[:mode]
  type = msg[:type]
  identifier = msg[:identifier]

  case mode
  when :read
    "#{"#{identifier} = " if identifier}r_#{type}"
  when :write
    "w_#{type}(#{identifier})"
  else
    raise "Unsupported message msg mode: `#{mode}`"
  end
end
_list_types(endian_marker) click to toggle source

This includes str, and anything with ‘*_list’ in the type name

# File lib/nmspec/ruby.rb, line 230
def _list_types(endian_marker)
  code = []

  code << '  ###########################################'
  code << '  # list types'
  code << '  ###########################################'
  code << ''

  ::Nmspec::V1::BASE_TYPES
    .each do |type|
      # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
      num_bytes, pack_type =  case type
                              when 'float_list'
                                [4, endian_marker.eql?('>') ? 'g' : 'e']
                              when 'double_list'
                                [8, endian_marker.eql?('>') ? 'G' : 'E']
                              when 'i8_list','u8_list'
                                [1, type.start_with?('i') ? 'c' : 'C']
                              when 'i16_list','u16_list'
                                [2, type.start_with?('i') ? "s#{endian_marker}" : "S#{endian_marker}"]
                              when 'i32_list','u32_list'
                                [4, type.start_with?('i') ? "l#{endian_marker}" : "L#{endian_marker}"]
                              when 'i64_list','u64_list'
                                [8, type.start_with?('i') ? "q#{endian_marker}" : "Q#{endian_marker}"]
                              else
                                next
                              end

      code << _type_list_reader_writer_methods(type, num_bytes, endian_marker, pack_type)
    end

  code
end
_numeric_types(endian_marker) click to toggle source

inserts the boilerplate base type readers and writers

# File lib/nmspec/ruby.rb, line 137
def _numeric_types(endian_marker)
  code = []

  code << '  ###########################################'
  code << '  # numeric types'
  code << '  ###########################################'
  code << ''

  ::Nmspec::V1::BASE_TYPES
    .each do |type|
      # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
      num_bytes, pack_type =  case type
                              when 'float'
                                [4, endian_marker.eql?('>') ? 'g' : 'e']
                              when 'double'
                                [8, endian_marker.eql?('>') ? 'G' : 'E']
                              when 'i8','u8'
                                [1, type.start_with?('i') ? 'c' : 'C']
                              when 'i16','u16'
                                [2, type.start_with?('i') ? "s#{endian_marker}" : "S#{endian_marker}"]
                              when 'i32','u32'
                                [4, type.start_with?('i') ? "l#{endian_marker}" : "L#{endian_marker}"]
                              when 'i64','u64'
                                [8, type.start_with?('i') ? "q#{endian_marker}" : "Q#{endian_marker}"]
                              else
                                next
                              end

      code << _type_reader_writer_methods(type, num_bytes, pack_type)
    end

  code
end
_opcode_mappings(protos) click to toggle source
# File lib/nmspec/ruby.rb, line 59
def _opcode_mappings(protos)
  code = []

  code << '  PROTO_TO_OP = {'
  code += protos.map.with_index{|p, i| "    '#{p[:name]}' => #{i}," }
  code << '  }'

  code << ''

  code << '  OP_TO_PROTO = {'
  code += protos.map.with_index{|p, i| "    #{i} => '#{p[:name]}'," }
  code << '  }'

  code
end
_proto_method(kind, proto_code, proto, local_vars, passed_params) click to toggle source

Builds a single protocol method

# File lib/nmspec/ruby.rb, line 356
def _proto_method(kind, proto_code, proto, local_vars, passed_params)
  code = []

  code << "  # #{proto[:desc]}" if proto[:desc]
  unless local_vars.empty?
    code << '  #'
    code << '  # returns:  (type | local var name)'
    code << '  # ['
    local_vars.uniq.each{|v| code << "  #    #{"#{v.first}".ljust(12)} | #{v.last}" }
    code << '  # ]'
  end

  code << "  def #{kind}_#{proto[:name]}#{passed_params.length > 0 ? "(#{(passed_params.to_a).join(', ')})" : ''}"

  msgs = proto[:msgs]
  code << "    w_u8(#{proto_code})" if kind.eql?('send')
  msgs.each do |msg|
    msg = kind.eql?('send') ? msg : _flip_mode(msg)
    code << "    #{_line_from_msg(msg)}"
  end
  code << "    [#{local_vars.map{|v| v.last }.uniq.join(', ')}]"
  code << "  end"

  code
end
_protos_methods(protos=[]) click to toggle source

builds all msg methods

# File lib/nmspec/ruby.rb, line 286
def _protos_methods(protos=[])
  code = []

  return code unless protos && protos&.length > 0

  code << '  ###########################################'
  code << '  # messages'
  code << '  ###########################################'

  protos.each_with_index do |proto, proto_code|
    # This figures out which identifiers mentioned in the msg
    # definition must be passed in vs. declared within the method

    code << ''
    send_local_vars = []
    recv_local_vars = []
    send_passed_params, recv_passed_params = proto[:msgs]
      .inject([Set.new, Set.new]) do |all_params, msg|
        send_params, recv_params = all_params

        mode = msg[:mode]
        type = msg[:type]
        identifier = msg[:identifier]

        case mode
        when :read
          send_local_vars << [type, identifier]
          recv_params << identifier unless recv_local_vars.map{|v| v.last}.include?(identifier)
        when :write
          recv_local_vars << [type, identifier]
          send_params << identifier unless send_local_vars.map{|v| v.last}.include?(identifier)
        else
          raise "Unsupported mode: `#{mode}`"
        end

        [send_params, recv_params]
      end

    ##
    # send
    code << _proto_method('send', proto_code, proto, send_local_vars, send_passed_params)
    code << ''
    code << _proto_method('recv', proto_code, proto, recv_local_vars, recv_passed_params)
  end

  if protos.length > 0
    code << ''
    code << '  # This method is used when you\'re receiving protocol messages'
    code << '  # in an unknown order, and dispatching automatically.'
    code << '  #'
    code << "  # NOTE: while you can pass parameters into this method, if you know the"
    code << "  #       inputs to what you want to receive then you probably know what"
    code << "  #       messages you are getting. In that case, explicit recv_* method calls"
    code << "  #       are preferred, if possible. However, this method can be very"
    code << "  #       effective for streaming in read-only protocol messages."
    code << '  def recv_any(params=[])'
    code << "    case @socket.recv(1).unpack('C').first"

    protos.each_with_index do |proto, proto_code|
      code << "    when #{proto_code} then [#{proto_code}, recv_#{proto[:name]}(*params)]"
    end

    code << '    end'
    code << '  end'
  end

  code
end
_str_types(endian_marker) click to toggle source
# File lib/nmspec/ruby.rb, line 189
def _str_types(endian_marker)
  code = []

  code << '  ###########################################'
  code << '  # str types'
  code << '  ###########################################'
  code << ''
  code << "  def r_str"
  code << "    bytes = @socket.recv(4).unpack('L#{endian_marker}').first"
  code << "    @socket.recv(bytes)"
  code << '  end'
  code << ''
  code << "  def w_str(str)"
  code << "    @socket.send([str.length].pack('L#{endian_marker}'), 0)"
  code << "    @socket.send(str, 0)"
  code << '  end'
  code << ''
  code << "  def r_str_list"
  code << '    strings = []'
  code << ''
  code << "    @socket.recv(4).unpack('L#{endian_marker}').first.times do"
  code << "      str_length = @socket.recv(4).unpack('L#{endian_marker}').first"
  code << "      strings << @socket.recv(str_length)"
  code << '    end'
  code << ''
  code << '    strings'
  code << '  end'
  code << ''
  code << "  def w_str_list(str_list)"
  code << "    @socket.send([str_list.length].pack('L#{endian_marker}'), 0)"
  code << '    str_list.each do |str|'
  code << "      @socket.send([str.length].pack('L#{endian_marker}'), 0)"
  code << "      @socket.send(str, 0)"
  code << '    end'
  code << '  end'
  code << ''

  code
end
_subtype_aliases(types) click to toggle source
# File lib/nmspec/ruby.rb, line 98
def _subtype_aliases(types)
  return unless types.detect{|t| !t[:base_type].nil? }
  code = []

  code << '  ###########################################'
  code << '  # subtype aliases'
  code << '  ###########################################'
  code << ''
  types.each do |type_hash|
    next unless type_hash[:base_type]
    code << "  alias_method :r_#{type_hash[:name]}, :r_#{type_hash[:base_type]}"
    code << "  alias_method :w_#{type_hash[:name]}, :w_#{type_hash[:base_type]}"
  end

  code
end
_type_list_reader_writer_methods(type, num_bytes, endian_marker, pack_type=nil) click to toggle source
# File lib/nmspec/ruby.rb, line 264
def _type_list_reader_writer_methods(type, num_bytes, endian_marker, pack_type=nil)
  code = []

  send_contents = pack_type ?  "(#{type}.pack('#{pack_type}*'), 0)" : "(#{type}, 0)"
  recv_contents = pack_type ? "(#{num_bytes} * #{type}.length).unpack('#{pack_type}*')" : "(#{num_bytes})"

  code << "  def r_#{type}"
  code << "    list_len = @socket.recv(4).unpack('L#{endian_marker}').first"
  code << "    @socket.recv(list_len * #{num_bytes}).unpack('#{pack_type}*')"
  code << '  end'
  code << ''
  code << "  def w_#{type}(#{type})"
  code << "    @socket.send([#{type}.length].pack('L#{endian_marker}'), 0)"
  code << "    @socket.send(#{type}.pack('#{pack_type}*'), 0)"
  code << '  end'
  code << ''

  code
end
_type_reader_writer_methods(type, num_bytes, pack_type=nil) click to toggle source
# File lib/nmspec/ruby.rb, line 171
def _type_reader_writer_methods(type, num_bytes, pack_type=nil)
  code = []

  send_contents = pack_type ?  "([#{type}].pack('#{pack_type}'), 0)" : "(#{type}, 0)"
  recv_contents = pack_type ? "(#{num_bytes}).unpack('#{pack_type}')" : "(#{num_bytes})"

  code << "  def r_#{type}"
  code << "    @socket.recv#{recv_contents}.first"
  code << '  end'
  code << ''
  code << "  def w_#{type}(#{type})"
  code << "    @socket.send#{send_contents}"
  code << '  end'
  code << ''

  code
end
gen(spec) click to toggle source
# File lib/nmspec/ruby.rb, line 7
def gen(spec)
  endian_marker = spec.dig(:msgr, :bigendian) ? '>' : '<'

  code = []
  code << "require 'socket'"
  code << ''
  code << '##'
  code << '# NOTE: this code is auto-generated from an nmspec file'

  if spec.dig(:msgr, :desc)
    code << '#'
    code << "# #{spec.dig(:msgr, :desc)}"
  end

  code << "class #{_class_name_from_msgr_name(spec.dig(:msgr, :name))}"

  if (spec[:protos]&.length || 0) > 0
    code << _opcode_mappings(spec[:protos])
    code << ''
  end

  code << _initialize
  code << ''
  code << _close
  code << ''
  code << _bool_type
  code << ''
  code << _numeric_types(endian_marker)
  code << _str_types(endian_marker)
  code << _list_types(endian_marker)

  types = spec[:types]
  code << _subtype_aliases(types)
  code << _protos_methods(spec[:protos])

  code << "end"

  code.join("\n")
rescue => e
  "Code generation failed due to unknown error: check spec validity\n  cause: #{e.inspect}"
  puts e.backtrace.join("\n  ")
end