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.
- checksums.yaml +4 -4
- data/LICENSE +19 -0
- data/README.md +301 -0
- data/lib/nmspec/gdscript.rb +392 -0
- data/lib/nmspec/parser.rb +84 -0
- data/lib/nmspec/ruby.rb +403 -0
- data/lib/nmspec/v1.rb +188 -0
- data/lib/nmspec/version.rb +1 -0
- metadata +10 -2
data/lib/nmspec/ruby.rb
ADDED
@@ -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.
|
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:
|
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
|