nmspec 1.5.0.pre → 1.5.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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