module Nmspec::V1

Constants

BASE_TYPES
GEN_OPTS_KEYS
IDENTIFIER_PATTERN
OPT_KEYS
REQ_KEYS
STR_TYPE_PATTERN
SUPPORTED_OUTPUT_LANGS
SUPPORTED_SPEC_VERSIONS
VALID_STEP_MODES

Public Class Methods

_sub_type?(type, all_types_so_far) click to toggle source
# File lib/nmspec/v1.rb, line 181
def _sub_type?(type, all_types_so_far)
  return false unless type.is_a?(String)

  type =~ /\A(#{all_types_so_far.join('|')})[0-9]*\Z/
end
_valid_msgr_name?(mod) click to toggle source
# File lib/nmspec/v1.rb, line 176
def _valid_msgr_name?(mod)
  return false unless mod.is_a?(Hash)
  mod['name'] =~ /\A[a-zA-Z][a-zA-Z_0-9\s]+\Z/
end
_valid_type?(type, all_types) click to toggle source
# File lib/nmspec/v1.rb, line 172
def _valid_type?(type, all_types)
  all_types.include?(type) || _sub_type?(type, all_types)
end
errors(raw_spec) click to toggle source
# File lib/nmspec/v1.rb, line 85
def errors(raw_spec)
  [].tap do |errors|
    ##
    # main keys
    REQ_KEYS.each do |k|
      errors << "required key `#{k}` is missing" unless raw_spec.has_key?(k)
    end

    unsupported_keys = raw_spec.keys - REQ_KEYS - OPT_KEYS
    errors << "spec contains unsupported keys: [#{unsupported_keys.join(', ')}]" unless unsupported_keys.empty?

    ##
    # msgr validation
    errors << "invalid msgr name" unless _valid_msgr_name?(raw_spec['msgr'])
    errors << 'msgr is missing a name' unless raw_spec['msgr'].is_a?(Hash) && raw_spec['msgr'].has_key?('name')

    ##
    # version check
    errors << "unsupported spec version: `#{raw_spec['version']}`" unless SUPPORTED_SPEC_VERSIONS.include?(raw_spec['version'])
    errors << "spec version must be a number" unless raw_spec['version'].is_a?(Integer)

    ##
    # type validation
    all_types = BASE_TYPES.dup
    raw_spec['types']&.each do |type_spec|
      type, name = type_spec.split

      errors << "invalid type name `#{name}`" unless name =~ IDENTIFIER_PATTERN
      if _valid_type?(type, all_types)
        all_types << name
      else
        errors << "type `#{name}` has an invalid subtype of `#{type}`"
      end
    end

    ##
    # msg validation
    protos = raw_spec['protos']
    protos&.each do |proto|
      errors << "invalid msg name `#{proto['name']}`" unless proto['name'] =~ IDENTIFIER_PATTERN
      proto['msgs'] || [].each do |msg|
        mode, type, identifier = msg.split.map(&:strip)
        short_msg = [mode, type, identifier].join(' ')
        errors << "msg `#{proto['name']}`, msg `#{short_msg}` has invalid type: `#{type}`" unless _valid_type?(type, all_types)

        case mode
        when 'r'
          errors << "reader protocol `#{proto['name']}`, msg `#{short_msg}` has missing identifier" if identifier.to_s.empty?
          errors << "reader protocol `#{proto['name']}`, msg `#{short_msg}` has invalid identifier: `#{identifier}`" unless identifier =~ IDENTIFIER_PATTERN
        when 'w'
          errors << "writer protocol `#{proto['name']}`, msg `#{short_msg}` has missing identifier" if identifier.to_s.empty?
          errors << "writer msg `#{proto['name']}`, msg `#{short_msg}` has invalid identifier: `#{identifier}`" unless identifier =~ IDENTIFIER_PATTERN
        else
          errors << "protocol `#{proto['name']}` has invalid read/write mode (#{mode}) - msg: `#{short_msg}`" unless VALID_STEP_MODES.include?(mode)
        end
      end
    end
  end
end
gen(opts) click to toggle source

Accepts a hash of options following this format:

{

'spec' => <String of valid nmspec YAML,
'langs' => ['ruby', ...],   # array of target languages

}

Returns a hash with this format:

# File lib/nmspec/v1.rb, line 34
def gen(opts)
  errors = []
  warnings = []

  errors << "invalid opts (expecting Hash, got #{opts.class})" unless opts.is_a?(Hash)
  errors << "unexpected keys in nmspec: [#{(opts.keys - GEN_OPTS_KEYS).join(', ')}]" unless (opts.keys - GEN_OPTS_KEYS).empty?
  errors << '`spec` key mising from nmspec options' unless opts.has_key?('spec')
  errors << "invalid spec (expecting valid nmspec YAML)" unless opts.dig('spec').is_a?(String)
  errors << '`langs` key missing' unless opts.has_key?('langs')
  errors << 'list of output languages cannot be empty' if opts.dig('langs')&.empty?
  errors << "invalid list of output languages (expecting array of strings)" unless opts.dig('langs').is_a?(Array) && opts.dig('langs').all?{|l| l.is_a?(String) }
  errors << "invalid output language(s): [#{(opts.dig('langs') || []).select{|l| !SUPPORTED_OUTPUT_LANGS.include?(l) }.join(', ') }] - valid options are [#{SUPPORTED_OUTPUT_LANGS.map{|l| "\"#{l}\"" }.join(', ')}]" unless opts.dig('langs')&.all?{|l| SUPPORTED_OUTPUT_LANGS.include?(l) }

  begin
    YAML.load(opts['spec']).is_a?(Hash)
  rescue TypeError
    errors << "invalid nmspec YAML, check format"
  end

  return ({
    'nmspec_lib_version' => NMSPEC_GEM_VERSION,
    'valid' => false,
    'errors' => errors,
    'warnings' => warnings,
    'code' => {}
  }) unless errors.empty?

  raw_spec = YAML.load(opts['spec'])
  langs = opts['langs']

  raise "spec failed to parse as valid YAML" unless raw_spec

  valid = Nmspec::V1.valid?(raw_spec)
  errors = Nmspec::V1.errors(raw_spec)
  warnings = Nmspec::V1.warnings(raw_spec)

  spec_hash = Nmspec::Parser.parse(raw_spec)
  code = langs.each_with_object({}) do |lang, hash|
    hash[lang] = send("to_#{lang}", spec_hash)
    hash
  end

  {
    'nmspec_lib_version' => NMSPEC_GEM_VERSION,
    'valid' => valid,
    'errors' => errors,
    'warnings' => warnings,
    'code' => code
  }
end
to_gdscript(spec_hash) click to toggle source
# File lib/nmspec/v1.rb, line 166
def to_gdscript(spec_hash)
  ::Nmspec::GDScript.gen(spec_hash)
rescue
  'codegen failure'
end
to_ruby(spec_hash) click to toggle source
# File lib/nmspec/v1.rb, line 160
def to_ruby(spec_hash)
  ::Nmspec::Ruby.gen(spec_hash)
rescue
  'codegen failure'
end
valid?(raw_spec) click to toggle source
# File lib/nmspec/v1.rb, line 156
def valid?(raw_spec)
  errors(raw_spec).empty?
end
warnings(raw_spec) click to toggle source
# File lib/nmspec/v1.rb, line 145
def warnings(raw_spec)
  [].tap do |warnings|
    warnings << 'msgr is missing a description' unless raw_spec['msgr'].is_a?(Hash) && raw_spec['msgr'].has_key?('desc')

    raw_spec['protos']&.each do |proto|
      warnings << "protocol `#{proto['name']}` has no msgs" if proto['msgs']&.empty?
      warnings << "msg `#{proto['name']}` is missing a description" unless proto.has_key?('desc')
    end
  end
end