nmspec 1.5.0.pre → 1.5.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,403 @@
1
+ require 'set'
2
+
3
+ # Nmspec code generator for ruby
4
+ module Nmspec
5
+ module Ruby
6
+ class << self
7
+ def gen(spec)
8
+ endian_marker = spec.dig(:msgr, :bigendian) ? '>' : '<'
9
+
10
+ code = []
11
+ code << "require 'socket'"
12
+ code << ''
13
+ code << '##'
14
+ code << '# NOTE: this code is auto-generated from an nmspec file'
15
+
16
+ if spec.dig(:msgr, :desc)
17
+ code << '#'
18
+ code << "# #{spec.dig(:msgr, :desc)}"
19
+ end
20
+
21
+ code << "class #{_class_name_from_msgr_name(spec.dig(:msgr, :name))}"
22
+
23
+ if (spec[:protos]&.length || 0) > 0
24
+ code << _opcode_mappings(spec[:protos])
25
+ code << ''
26
+ end
27
+
28
+ code << _initialize
29
+ code << ''
30
+ code << _close
31
+ code << ''
32
+ code << _bool_type
33
+ code << ''
34
+ code << _numeric_types(endian_marker)
35
+ code << _str_types(endian_marker)
36
+ code << _list_types(endian_marker)
37
+
38
+ types = spec[:types]
39
+ code << _subtype_aliases(types)
40
+ code << _protos_methods(spec[:protos])
41
+
42
+ code << "end"
43
+
44
+ code.join("\n")
45
+ rescue => e
46
+ "Code generation failed due to unknown error: check spec validity\n cause: #{e.inspect}"
47
+ puts e.backtrace.join("\n ")
48
+ end
49
+
50
+ def _class_name_from_msgr_name(name)
51
+ name
52
+ .downcase
53
+ .gsub(/[\._\-]/, ' ')
54
+ .split(' ')
55
+ .map{|part| part.capitalize}
56
+ .join + 'Msgr'
57
+ end
58
+
59
+ def _opcode_mappings(protos)
60
+ code = []
61
+
62
+ code << ' PROTO_TO_OP = {'
63
+ code += protos.map.with_index{|p, i| " '#{p[:name]}' => #{i}," }
64
+ code << ' }'
65
+
66
+ code << ''
67
+
68
+ code << ' OP_TO_PROTO = {'
69
+ code += protos.map.with_index{|p, i| " #{i} => '#{p[:name]}'," }
70
+ code << ' }'
71
+
72
+ code
73
+ end
74
+
75
+ def _initialize
76
+ code = []
77
+
78
+ code << ' def initialize(socket, no_delay=false)'
79
+ code << ' @socket = socket'
80
+ code << ' @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if no_delay'
81
+ code << ' end'
82
+
83
+ code
84
+ end
85
+
86
+ def _close
87
+ code = []
88
+
89
+ code << ' ##'
90
+ code << ' # closes the socket inside this object'
91
+ code << ' def close'
92
+ code << ' @socket&.close'
93
+ code << ' end'
94
+
95
+ code
96
+ end
97
+
98
+ def _subtype_aliases(types)
99
+ return unless types.detect{|t| !t[:base_type].nil? }
100
+ code = []
101
+
102
+ code << ' ###########################################'
103
+ code << ' # subtype aliases'
104
+ code << ' ###########################################'
105
+ code << ''
106
+ types.each do |type_hash|
107
+ next unless type_hash[:base_type]
108
+ code << " alias_method :r_#{type_hash[:name]}, :r_#{type_hash[:base_type]}"
109
+ code << " alias_method :w_#{type_hash[:name]}, :w_#{type_hash[:base_type]}"
110
+ end
111
+
112
+ code
113
+ end
114
+
115
+ ##
116
+ # inserts the boolean type readers and writers
117
+ def _bool_type
118
+ code = []
119
+
120
+ code << ' ###########################################'
121
+ code << ' # boolean type'
122
+ code << ' ###########################################'
123
+ code << ''
124
+ code << " def r_bool"
125
+ code << " @socket.recv(1).unpack('C')[0] == 1"
126
+ code << ' end'
127
+ code << ''
128
+ code << " def w_bool(bool)"
129
+ code << " @socket.send([bool ? 1 : 0].pack('C'), 0)"
130
+ code << ' end'
131
+
132
+ code
133
+ end
134
+
135
+ ##
136
+ # inserts the boilerplate base type readers and writers
137
+ def _numeric_types(endian_marker)
138
+ code = []
139
+
140
+ code << ' ###########################################'
141
+ code << ' # numeric types'
142
+ code << ' ###########################################'
143
+ code << ''
144
+
145
+ ::Nmspec::V1::BASE_TYPES
146
+ .each do |type|
147
+ # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
148
+ num_bytes, pack_type = case type
149
+ when 'float'
150
+ [4, endian_marker.eql?('>') ? 'g' : 'e']
151
+ when 'double'
152
+ [8, endian_marker.eql?('>') ? 'G' : 'E']
153
+ when 'i8','u8'
154
+ [1, type.start_with?('i') ? 'c' : 'C']
155
+ when 'i16','u16'
156
+ [2, type.start_with?('i') ? "s#{endian_marker}" : "S#{endian_marker}"]
157
+ when 'i32','u32'
158
+ [4, type.start_with?('i') ? "l#{endian_marker}" : "L#{endian_marker}"]
159
+ when 'i64','u64'
160
+ [8, type.start_with?('i') ? "q#{endian_marker}" : "Q#{endian_marker}"]
161
+ else
162
+ next
163
+ end
164
+
165
+ code << _type_reader_writer_methods(type, num_bytes, pack_type)
166
+ end
167
+
168
+ code
169
+ end
170
+
171
+ def _type_reader_writer_methods(type, num_bytes, pack_type=nil)
172
+ code = []
173
+
174
+ send_contents = pack_type ? "([#{type}].pack('#{pack_type}'), 0)" : "(#{type}, 0)"
175
+ recv_contents = pack_type ? "(#{num_bytes}).unpack('#{pack_type}')" : "(#{num_bytes})"
176
+
177
+ code << " def r_#{type}"
178
+ code << " @socket.recv#{recv_contents}.first"
179
+ code << ' end'
180
+ code << ''
181
+ code << " def w_#{type}(#{type})"
182
+ code << " @socket.send#{send_contents}"
183
+ code << ' end'
184
+ code << ''
185
+
186
+ code
187
+ end
188
+
189
+ def _str_types(endian_marker)
190
+ code = []
191
+
192
+ code << ' ###########################################'
193
+ code << ' # str types'
194
+ code << ' ###########################################'
195
+ code << ''
196
+ code << " def r_str"
197
+ code << " bytes = @socket.recv(4).unpack('L#{endian_marker}').first"
198
+ code << " @socket.recv(bytes)"
199
+ code << ' end'
200
+ code << ''
201
+ code << " def w_str(str)"
202
+ code << " @socket.send([str.length].pack('L#{endian_marker}'), 0)"
203
+ code << " @socket.send(str, 0)"
204
+ code << ' end'
205
+ code << ''
206
+ code << " def r_str_list"
207
+ code << ' strings = []'
208
+ code << ''
209
+ code << " @socket.recv(4).unpack('L#{endian_marker}').first.times do"
210
+ code << " str_length = @socket.recv(4).unpack('L#{endian_marker}').first"
211
+ code << " strings << @socket.recv(str_length)"
212
+ code << ' end'
213
+ code << ''
214
+ code << ' strings'
215
+ code << ' end'
216
+ code << ''
217
+ code << " def w_str_list(str_list)"
218
+ code << " @socket.send([str_list.length].pack('L#{endian_marker}'), 0)"
219
+ code << ' str_list.each do |str|'
220
+ code << " @socket.send([str.length].pack('L#{endian_marker}'), 0)"
221
+ code << " @socket.send(str, 0)"
222
+ code << ' end'
223
+ code << ' end'
224
+ code << ''
225
+
226
+ code
227
+ end
228
+
229
+ # This includes str, and anything with '*_list' in the type name
230
+ def _list_types(endian_marker)
231
+ code = []
232
+
233
+ code << ' ###########################################'
234
+ code << ' # list types'
235
+ code << ' ###########################################'
236
+ code << ''
237
+
238
+ ::Nmspec::V1::BASE_TYPES
239
+ .each do |type|
240
+ # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
241
+ num_bytes, pack_type = case type
242
+ when 'float_list'
243
+ [4, endian_marker.eql?('>') ? 'g' : 'e']
244
+ when 'double_list'
245
+ [8, endian_marker.eql?('>') ? 'G' : 'E']
246
+ when 'i8_list','u8_list'
247
+ [1, type.start_with?('i') ? 'c' : 'C']
248
+ when 'i16_list','u16_list'
249
+ [2, type.start_with?('i') ? "s#{endian_marker}" : "S#{endian_marker}"]
250
+ when 'i32_list','u32_list'
251
+ [4, type.start_with?('i') ? "l#{endian_marker}" : "L#{endian_marker}"]
252
+ when 'i64_list','u64_list'
253
+ [8, type.start_with?('i') ? "q#{endian_marker}" : "Q#{endian_marker}"]
254
+ else
255
+ next
256
+ end
257
+
258
+ code << _type_list_reader_writer_methods(type, num_bytes, endian_marker, pack_type)
259
+ end
260
+
261
+ code
262
+ end
263
+
264
+ def _type_list_reader_writer_methods(type, num_bytes, endian_marker, pack_type=nil)
265
+ code = []
266
+
267
+ send_contents = pack_type ? "(#{type}.pack('#{pack_type}*'), 0)" : "(#{type}, 0)"
268
+ recv_contents = pack_type ? "(#{num_bytes} * #{type}.length).unpack('#{pack_type}*')" : "(#{num_bytes})"
269
+
270
+ code << " def r_#{type}"
271
+ code << " list_len = @socket.recv(4).unpack('L#{endian_marker}').first"
272
+ code << " @socket.recv(list_len * #{num_bytes}).unpack('#{pack_type}*')"
273
+ code << ' end'
274
+ code << ''
275
+ code << " def w_#{type}(#{type})"
276
+ code << " @socket.send([#{type}.length].pack('L#{endian_marker}'), 0)"
277
+ code << " @socket.send(#{type}.pack('#{pack_type}*'), 0)"
278
+ code << ' end'
279
+ code << ''
280
+
281
+ code
282
+ end
283
+
284
+ ##
285
+ # builds all msg methods
286
+ def _protos_methods(protos=[])
287
+ code = []
288
+
289
+ return code unless protos && protos&.length > 0
290
+
291
+ code << ' ###########################################'
292
+ code << ' # messages'
293
+ code << ' ###########################################'
294
+
295
+ protos.each_with_index do |proto, proto_code|
296
+ # This figures out which identifiers mentioned in the msg
297
+ # definition must be passed in vs. declared within the method
298
+
299
+ code << ''
300
+ send_local_vars = []
301
+ recv_local_vars = []
302
+ send_passed_params, recv_passed_params = proto[:msgs]
303
+ .inject([Set.new, Set.new]) do |all_params, msg|
304
+ send_params, recv_params = all_params
305
+
306
+ mode = msg[:mode]
307
+ type = msg[:type]
308
+ identifier = msg[:identifier]
309
+
310
+ case mode
311
+ when :read
312
+ send_local_vars << [type, identifier]
313
+ recv_params << identifier unless recv_local_vars.map{|v| v.last}.include?(identifier)
314
+ when :write
315
+ recv_local_vars << [type, identifier]
316
+ send_params << identifier unless send_local_vars.map{|v| v.last}.include?(identifier)
317
+ else
318
+ raise "Unsupported mode: `#{mode}`"
319
+ end
320
+
321
+ [send_params, recv_params]
322
+ end
323
+
324
+ ##
325
+ # send
326
+ code << _proto_method('send', proto_code, proto, send_local_vars, send_passed_params)
327
+ code << ''
328
+ code << _proto_method('recv', proto_code, proto, recv_local_vars, recv_passed_params)
329
+ end
330
+
331
+ if protos.length > 0
332
+ code << ''
333
+ code << ' # This method is used when you\'re receiving protocol messages'
334
+ code << ' # in an unknown order, and dispatching automatically.'
335
+ code << ' #'
336
+ code << " # NOTE: while you can pass parameters into this method, if you know the"
337
+ code << " # inputs to what you want to receive then you probably know what"
338
+ code << " # messages you are getting. In that case, explicit recv_* method calls"
339
+ code << " # are preferred, if possible. However, this method can be very"
340
+ code << " # effective for streaming in read-only protocol messages."
341
+ code << ' def recv_any(params=[])'
342
+ code << " case @socket.recv(1).unpack('C').first"
343
+
344
+ protos.each_with_index do |proto, proto_code|
345
+ code << " when #{proto_code} then [#{proto_code}, recv_#{proto[:name]}(*params)]"
346
+ end
347
+
348
+ code << ' end'
349
+ code << ' end'
350
+ end
351
+
352
+ code
353
+ end
354
+ ##
355
+ # Builds a single protocol method
356
+ def _proto_method(kind, proto_code, proto, local_vars, passed_params)
357
+ code = []
358
+
359
+ code << " # #{proto[:desc]}" if proto[:desc]
360
+ unless local_vars.empty?
361
+ code << ' #'
362
+ code << ' # returns: (type | local var name)'
363
+ code << ' # ['
364
+ local_vars.uniq.each{|v| code << " # #{"#{v.first}".ljust(12)} | #{v.last}" }
365
+ code << ' # ]'
366
+ end
367
+
368
+ code << " def #{kind}_#{proto[:name]}#{passed_params.length > 0 ? "(#{(passed_params.to_a).join(', ')})" : ''}"
369
+
370
+ msgs = proto[:msgs]
371
+ code << " w_u8(#{proto_code})" if kind.eql?('send')
372
+ msgs.each do |msg|
373
+ msg = kind.eql?('send') ? msg : _flip_mode(msg)
374
+ code << " #{_line_from_msg(msg)}"
375
+ end
376
+ code << " [#{local_vars.map{|v| v.last }.uniq.join(', ')}]"
377
+ code << " end"
378
+
379
+ code
380
+ end
381
+
382
+ def _flip_mode(msg)
383
+ opposite_mode = msg[:mode] == :read ? :write : :read
384
+ { mode: opposite_mode, type: msg[:type], identifier: msg[:identifier] }
385
+ end
386
+
387
+ def _line_from_msg(msg)
388
+ mode = msg[:mode]
389
+ type = msg[:type]
390
+ identifier = msg[:identifier]
391
+
392
+ case mode
393
+ when :read
394
+ "#{"#{identifier} = " if identifier}r_#{type}"
395
+ when :write
396
+ "w_#{type}(#{identifier})"
397
+ else
398
+ raise "Unsupported message msg mode: `#{mode}`"
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
data/lib/nmspec/v1.rb ADDED
@@ -0,0 +1,188 @@
1
+ require 'yaml'
2
+ require_relative './version.rb'
3
+
4
+ module Nmspec
5
+ module V1
6
+ SUPPORTED_SPEC_VERSIONS = [1]
7
+ SUPPORTED_OUTPUT_LANGS = %w(gdscript ruby)
8
+ GEN_OPTS_KEYS = %w(langs spec)
9
+ REQ_KEYS = %w(version msgr)
10
+ OPT_KEYS = %w(types protos)
11
+ BASE_TYPES = %w(
12
+ i8 u8 i8_list u8_list
13
+ i16 u16 i16_list u16_list
14
+ i32 u32 i32_list u32_list
15
+ i64 u64 i64_list u64_list
16
+ float float_list
17
+ double double_list
18
+ str str_list
19
+ )
20
+
21
+ STR_TYPE_PATTERN = /\Astr[0-9]+\Z/
22
+ IDENTIFIER_PATTERN = /\A[_a-zA-Z][_a-zA-Z0-9]*\Z/
23
+ VALID_STEP_MODES = %w(r w)
24
+
25
+ class << self
26
+ # Accepts a hash of options following this format:
27
+ #
28
+ # {
29
+ # 'spec' => <String of valid nmspec YAML,
30
+ # 'langs' => ['ruby', ...], # array of target languages
31
+ # }
32
+ #
33
+ # Returns a hash with this format:
34
+ def gen(opts)
35
+ errors = []
36
+ warnings = []
37
+
38
+ errors << "invalid opts (expecting Hash, got #{opts.class})" unless opts.is_a?(Hash)
39
+ errors << "unexpected keys in nmspec: [#{(opts.keys - GEN_OPTS_KEYS).join(', ')}]" unless (opts.keys - GEN_OPTS_KEYS).empty?
40
+ errors << '`spec` key mising from nmspec options' unless opts.has_key?('spec')
41
+ errors << "invalid spec (expecting valid nmspec YAML)" unless opts.dig('spec').is_a?(String)
42
+ errors << '`langs` key missing' unless opts.has_key?('langs')
43
+ errors << 'list of output languages cannot be empty' if opts.dig('langs')&.empty?
44
+ 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) }
45
+ 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) }
46
+
47
+ begin
48
+ YAML.load(opts['spec']).is_a?(Hash)
49
+ rescue TypeError
50
+ errors << "invalid nmspec YAML, check format"
51
+ end
52
+
53
+ return ({
54
+ 'nmspec_lib_version' => NMSPEC_GEM_VERSION,
55
+ 'valid' => false,
56
+ 'errors' => errors,
57
+ 'warnings' => warnings,
58
+ 'code' => {}
59
+ }) unless errors.empty?
60
+
61
+ raw_spec = YAML.load(opts['spec'])
62
+ langs = opts['langs']
63
+
64
+ raise "spec failed to parse as valid YAML" unless raw_spec
65
+
66
+ valid = Nmspec::V1.valid?(raw_spec)
67
+ errors = Nmspec::V1.errors(raw_spec)
68
+ warnings = Nmspec::V1.warnings(raw_spec)
69
+
70
+ spec_hash = Nmspec::Parser.parse(raw_spec)
71
+ code = langs.each_with_object({}) do |lang, hash|
72
+ hash[lang] = send("to_#{lang}", spec_hash)
73
+ hash
74
+ end
75
+
76
+ {
77
+ 'nmspec_lib_version' => NMSPEC_GEM_VERSION,
78
+ 'valid' => valid,
79
+ 'errors' => errors,
80
+ 'warnings' => warnings,
81
+ 'code' => code
82
+ }
83
+ end
84
+
85
+ def errors(raw_spec)
86
+ [].tap do |errors|
87
+ ##
88
+ # main keys
89
+ REQ_KEYS.each do |k|
90
+ errors << "required key `#{k}` is missing" unless raw_spec.has_key?(k)
91
+ end
92
+
93
+ unsupported_keys = raw_spec.keys - REQ_KEYS - OPT_KEYS
94
+ errors << "spec contains unsupported keys: [#{unsupported_keys.join(', ')}]" unless unsupported_keys.empty?
95
+
96
+ ##
97
+ # msgr validation
98
+ errors << "invalid msgr name" unless _valid_msgr_name?(raw_spec['msgr'])
99
+ errors << 'msgr is missing a name' unless raw_spec['msgr'].is_a?(Hash) && raw_spec['msgr'].has_key?('name')
100
+
101
+ ##
102
+ # version check
103
+ errors << "unsupported spec version: `#{raw_spec['version']}`" unless SUPPORTED_SPEC_VERSIONS.include?(raw_spec['version'])
104
+ errors << "spec version must be a number" unless raw_spec['version'].is_a?(Integer)
105
+
106
+ ##
107
+ # type validation
108
+ all_types = BASE_TYPES.dup
109
+ raw_spec['types']&.each do |type_spec|
110
+ type, name = type_spec.split
111
+
112
+ errors << "invalid type name `#{name}`" unless name =~ IDENTIFIER_PATTERN
113
+ if _valid_type?(type, all_types)
114
+ all_types << name
115
+ else
116
+ errors << "type `#{name}` has an invalid subtype of `#{type}`"
117
+ end
118
+ end
119
+
120
+ ##
121
+ # msg validation
122
+ protos = raw_spec['protos']
123
+ protos&.each do |proto|
124
+ errors << "invalid msg name `#{proto['name']}`" unless proto['name'] =~ IDENTIFIER_PATTERN
125
+ proto['msgs'] || [].each do |msg|
126
+ mode, type, identifier = msg.split.map(&:strip)
127
+ short_msg = [mode, type, identifier].join(' ')
128
+ errors << "msg `#{proto['name']}`, msg `#{short_msg}` has invalid type: `#{type}`" unless _valid_type?(type, all_types)
129
+
130
+ case mode
131
+ when 'r'
132
+ errors << "reader protocol `#{proto['name']}`, msg `#{short_msg}` has missing identifier" if identifier.to_s.empty?
133
+ errors << "reader protocol `#{proto['name']}`, msg `#{short_msg}` has invalid identifier: `#{identifier}`" unless identifier =~ IDENTIFIER_PATTERN
134
+ when 'w'
135
+ errors << "writer protocol `#{proto['name']}`, msg `#{short_msg}` has missing identifier" if identifier.to_s.empty?
136
+ errors << "writer msg `#{proto['name']}`, msg `#{short_msg}` has invalid identifier: `#{identifier}`" unless identifier =~ IDENTIFIER_PATTERN
137
+ else
138
+ errors << "protocol `#{proto['name']}` has invalid read/write mode (#{mode}) - msg: `#{short_msg}`" unless VALID_STEP_MODES.include?(mode)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def warnings(raw_spec)
146
+ [].tap do |warnings|
147
+ warnings << 'msgr is missing a description' unless raw_spec['msgr'].is_a?(Hash) && raw_spec['msgr'].has_key?('desc')
148
+
149
+ raw_spec['protos']&.each do |proto|
150
+ warnings << "protocol `#{proto['name']}` has no msgs" if proto['msgs']&.empty?
151
+ warnings << "msg `#{proto['name']}` is missing a description" unless proto.has_key?('desc')
152
+ end
153
+ end
154
+ end
155
+
156
+ def valid?(raw_spec)
157
+ errors(raw_spec).empty?
158
+ end
159
+
160
+ def to_ruby(spec_hash)
161
+ ::Nmspec::Ruby.gen(spec_hash)
162
+ rescue
163
+ 'codegen failure'
164
+ end
165
+
166
+ def to_gdscript(spec_hash)
167
+ ::Nmspec::GDScript.gen(spec_hash)
168
+ rescue
169
+ 'codegen failure'
170
+ end
171
+
172
+ def _valid_type?(type, all_types)
173
+ all_types.include?(type) || _sub_type?(type, all_types)
174
+ end
175
+
176
+ def _valid_msgr_name?(mod)
177
+ return false unless mod.is_a?(Hash)
178
+ mod['name'] =~ /\A[a-zA-Z][a-zA-Z_0-9\s]+\Z/
179
+ end
180
+
181
+ def _sub_type?(type, all_types_so_far)
182
+ return false unless type.is_a?(String)
183
+
184
+ type =~ /\A(#{all_types_so_far.join('|')})[0-9]*\Z/
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1 @@
1
+ NMSPEC_GEM_VERSION = '1.5.0.pre2'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nmspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0.pre
4
+ version: 1.5.0.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Lunt
@@ -13,12 +13,20 @@ dependencies: []
13
13
  description: nmspec makes it easier to describe binary messages between two network
14
14
  peers via a config file, generate their network code in a number of languages, and
15
15
  keep their code in sync
16
- email: jefflunt@gmail.com
16
+ email:
17
+ - jefflunt@gmail.com
17
18
  executables: []
18
19
  extensions: []
19
20
  extra_rdoc_files: []
20
21
  files:
22
+ - LICENSE
23
+ - README.md
21
24
  - lib/nmspec.rb
25
+ - lib/nmspec/gdscript.rb
26
+ - lib/nmspec/parser.rb
27
+ - lib/nmspec/ruby.rb
28
+ - lib/nmspec/v1.rb
29
+ - lib/nmspec/version.rb
22
30
  homepage: http://nmspec.com
23
31
  licenses:
24
32
  - MIT